require 'date' module Aws module Plugins class ClientMetricsPlugin < Seahorse::Client::Plugin option(:client_side_monitoring, default: false, doc_type: 'Boolean', docstring: <<-DOCS) do |cfg| When `true`, client-side metrics will be collected for all API requests from this client. DOCS resolve_client_side_monitoring(cfg) end option(:client_side_monitoring_port, default: 31000, doc_type: Integer, docstring: <<-DOCS) do |cfg| Required for publishing client metrics. The port that the client side monitoring agent is running on, where client metrics will be published via UDP. DOCS resolve_client_side_monitoring_port(cfg) end option(:client_side_monitoring_publisher, default: ClientSideMonitoring::Publisher, doc_type: Aws::ClientSideMonitoring::Publisher, docstring: <<-DOCS) do |cfg| Allows you to provide a custom client-side monitoring publisher class. By default, will use the Client Side Monitoring Agent Publisher. DOCS resolve_publisher(cfg) end option(:client_side_monitoring_client_id, default: "", doc_type: String, docstring: <<-DOCS) do |cfg| Allows you to provide an identifier for this client which will be attached to all generated client side metrics. Defaults to an empty string. DOCS resolve_client_id(cfg) end def add_handlers(handlers, config) if config.client_side_monitoring && config.client_side_monitoring_port handlers.add(Handler, step: :initialize) publisher = config.client_side_monitoring_publisher publisher.agent_port = config.client_side_monitoring_port end end private def self.resolve_publisher(cfg) ClientSideMonitoring::Publisher.new end def self.resolve_client_side_monitoring_port(cfg) env_source = ENV["AWS_CSM_PORT"] env_source = nil if env_source == "" cfg_source = Aws.shared_config.csm_port(profile: cfg.profile) if env_source env_source.to_i elsif cfg_source cfg_source.to_i else 31000 end end def self.resolve_client_side_monitoring(cfg) env_source = ENV["AWS_CSM_ENABLED"] env_source = nil if env_source == "" if env_source.is_a?(String) && (env_source.downcase == "false" || env_source.downcase == "f") env_source = false end cfg_source = Aws.shared_config.csm_enabled(profile: cfg.profile) if env_source || cfg_source true else false end end def self.resolve_client_id(cfg) default = "" env_source = ENV["AWS_CSM_CLIENT_ID"] env_source = nil if env_source == "" cfg_source = Aws.shared_config.csm_client_id(profile: cfg.profile) env_source || cfg_source || default end class Handler < Seahorse::Client::Handler def call(context) publisher = context.config.client_side_monitoring_publisher service_id = context.config.api.metadata["serviceId"] # serviceId not present in all versions, need a fallback service_id ||= _calculate_service_id(context) request_metrics = ClientSideMonitoring::RequestMetrics.new( service: service_id, operation: context.operation.name, client_id: context.config.client_side_monitoring_client_id, region: context.config.region, timestamp: DateTime.now.strftime('%Q').to_i, ) context.metadata[:client_metrics] = request_metrics start_time = Aws::Util.monotonic_milliseconds final_error_retryable = false final_aws_exception = nil final_aws_exception_message = nil final_sdk_exception = nil final_sdk_exception_message = nil begin @handler.call(context) rescue StandardError => e # Handle SDK Exceptions inspector = Aws::Plugins::RetryErrors::ErrorInspector.new( e, context.http_response.status_code ) if inspector.retryable?(context) final_error_retryable = true end if request_metrics.api_call_attempts.empty? attempt = request_metrics.build_call_attempt attempt.sdk_exception = e.class.to_s attempt.sdk_exception_msg = e.message request_metrics.add_call_attempt(attempt) elsif request_metrics.api_call_attempts.last.aws_exception.nil? # Handle exceptions during response handlers attempt = request_metrics.api_call_attempts.last attempt.sdk_exception = e.class.to_s attempt.sdk_exception_msg = e.message elsif !e.class.to_s.match(request_metrics.api_call_attempts.last.aws_exception) # Handle response handling exceptions that happened in addition to # an AWS exception attempt = request_metrics.api_call_attempts.last attempt.sdk_exception = e.class.to_s attempt.sdk_exception_msg = e.message end # Else we don't have an SDK exception and are done. final_attempt = request_metrics.api_call_attempts.last final_aws_exception = final_attempt.aws_exception final_aws_exception_message = final_attempt.aws_exception_msg final_sdk_exception = final_attempt.sdk_exception final_sdk_exception_message = final_attempt.sdk_exception_msg raise e ensure end_time = Aws::Util.monotonic_milliseconds complete_opts = { latency: end_time - start_time, attempt_count: context.retries + 1, user_agent: context.http_request.headers["user-agent"], final_error_retryable: final_error_retryable, final_http_status_code: context.http_response.status_code, final_aws_exception: final_aws_exception, final_aws_exception_message: final_aws_exception_message, final_sdk_exception: final_sdk_exception, final_sdk_exception_message: final_sdk_exception_message } if context.metadata[:redirect_region] complete_opts[:region] = context.metadata[:redirect_region] end request_metrics.api_call.complete(complete_opts) # Report the metrics by passing the complete RequestMetrics object if publisher publisher.publish(request_metrics) end # Else we drop all this on the floor. end end private def _calculate_service_id(context) class_name = context.client.class.to_s.match(/(.+)::Client/)[1] class_name.sub!(/^Aws::/, '') _fallback_service_id(class_name) end def _fallback_service_id(id) # Need hard-coded exceptions since information needed to # reverse-engineer serviceId is not present in older versions. # This list should not need to grow. exceptions = { "ACMPCA" => "ACM PCA", "APIGateway" => "API Gateway", "AlexaForBusiness" => "Alexa For Business", "ApplicationAutoScaling" => "Application Auto Scaling", "ApplicationDiscoveryService" => "Application Discovery Service", "AutoScaling" => "Auto Scaling", "AutoScalingPlans" => "Auto Scaling Plans", "CloudHSMV2" => "CloudHSM V2", "CloudSearchDomain" => "CloudSearch Domain", "CloudWatchEvents" => "CloudWatch Events", "CloudWatchLogs" => "CloudWatch Logs", "CognitoIdentity" => "Cognito Identity", "CognitoIdentityProvider" => "Cognito Identity Provider", "CognitoSync" => "Cognito Sync", "ConfigService" => "Config Service", "CostExplorer" => "Cost Explorer", "CostandUsageReportService" => "Cost and Usage Report Service", "DataPipeline" => "Data Pipeline", "DatabaseMigrationService" => "Database Migration Service", "DeviceFarm" => "Device Farm", "DirectConnect" => "Direct Connect", "DirectoryService" => "Directory Service", "DynamoDBStreams" => "DynamoDB Streams", "ElasticBeanstalk" => "Elastic Beanstalk", "ElasticLoadBalancing" => "Elastic Load Balancing", "ElasticLoadBalancingV2" => "Elastic Load Balancing v2", "ElasticTranscoder" => "Elastic Transcoder", "ElasticsearchService" => "Elasticsearch Service", "IoTDataPlane" => "IoT Data Plane", "IoTJobsDataPlane" => "IoT Jobs Data Plane", "IoT1ClickDevicesService" => "IoT 1Click Devices Service", "IoT1ClickProjects" => "IoT 1Click Projects", "KinesisAnalytics" => "Kinesis Analytics", "KinesisVideo" => "Kinesis Video", "KinesisVideoArchivedMedia" => "Kinesis Video Archived Media", "KinesisVideoMedia" => "Kinesis Video Media", "LambdaPreview" => "Lambda", "Lex" => "Lex Runtime Service", "LexModelBuildingService" => "Lex Model Building Service", "Lightsail" => "Lightsail", "MQ" => "mq", "MachineLearning" => "Machine Learning", "MarketplaceCommerceAnalytics" => "Marketplace Commerce Analytics", "MarketplaceEntitlementService" => "Marketplace Entitlement Service", "MarketplaceMetering" => "Marketplace Metering", "MediaStoreData" => "MediaStore Data", "MigrationHub" => "Migration Hub", "ResourceGroups" => "Resource Groups", "ResourceGroupsTaggingAPI" => "Resource Groups Tagging API", "Route53" => "Route 53", "Route53Domains" => "Route 53 Domains", "SecretsManager" => "Secrets Manager", "SageMakerRuntime" => "SageMaker Runtime", "ServiceCatalog" => "Service Catalog", "ServiceDiscovery" => "ServiceDiscovery", "Signer" => "signer", "States" => "SFN", "StorageGateway" => "Storage Gateway", "TranscribeService" => "Transcribe Service", "WAFRegional" => "WAF Regional", } if exceptions[id] exceptions[id] else id end end end end end end