|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +# © 2023 SolarWinds Worldwide, LLC. All rights reserved. |
| 4 | +# |
| 5 | +# 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 |
| 6 | +# |
| 7 | +# 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. |
| 8 | + |
| 9 | +module SolarWindsAPM |
| 10 | + # OTLPEndPoint |
| 11 | + class OTLPEndPoint |
| 12 | + |
| 13 | + SW_ENDPOINT_REGEX = /^apm\.collector\.([a-z0-9-]+)\.cloud\.solarwinds\.com$/ |
| 14 | + OTEL_ENDPOINT_REGEX = %r{^https://otel\.collector\.([a-z0-9-]+)\.cloud\.solarwinds\.com:443(?:/.*)?$} |
| 15 | + OTEL_ENDPOINT_LOCAL_REGEX = %r{\Ahttp://0\.0\.0\.0:(4317|4318)\z} |
| 16 | + OTEL_ENDPOINT_LOCAL_REGEX2 = %r{\Ahttp://0\.0\.0\.0:(4317|4318)/v1/(metrics|traces|logs)\z} |
| 17 | + DEFAULT_OTLP_ENDPOINT = 'https://otel.collector.na-01.cloud.solarwinds.com:443' |
| 18 | + DEFAULT_APMPROTO_ENDPOINT = 'apm.collector.na-01.cloud.solarwinds.com' |
| 19 | + |
| 20 | + def initialize |
| 21 | + @token = nil |
| 22 | + @service_name = nil |
| 23 | + @lambda_env = determine_lambda_env |
| 24 | + @agent_enable = true |
| 25 | + @localhost = false |
| 26 | + determine_if_localhost |
| 27 | + end |
| 28 | + |
| 29 | + def determine_if_localhost |
| 30 | + @localhost = true if ENV['OTEL_EXPORTER_OTLP_ENDPOINT'].to_s.match?(OTEL_ENDPOINT_LOCAL_REGEX) |
| 31 | + @localhost = true if ENV['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'].to_s.match?(OTEL_ENDPOINT_LOCAL_REGEX2) |
| 32 | + @localhost = true if ENV['OTEL_EXPORTER_OTLP_METRICS_ENDPOINT'].to_s.match?(OTEL_ENDPOINT_LOCAL_REGEX2) |
| 33 | + @localhost = true if ENV['OTEL_EXPORTER_OTLP_LOGS_ENDPOINT'].to_s.match?(OTEL_ENDPOINT_LOCAL_REGEX2) |
| 34 | + end |
| 35 | + |
| 36 | + def config_otlp_endpoint |
| 37 | + config_service_name |
| 38 | + config_token |
| 39 | + ['TRACES','METRICS','LOGS'].each { |data_type| configure_otlp_endpoint(data_type) } |
| 40 | + end |
| 41 | + |
| 42 | + def config_service_name |
| 43 | + resource_attributes = ENV['OTEL_RESOURCE_ATTRIBUTES'].to_s.split(',').each_with_object({}) do |resource, hash| |
| 44 | + key, value = resource.split('=') |
| 45 | + hash[key] = value |
| 46 | + end |
| 47 | + |
| 48 | + unless @lambda_env |
| 49 | + @service_name = ENV['OTEL_SERVICE_NAME'] || resource_attributes['service.name'] || @service_name || 'None' |
| 50 | + else |
| 51 | + @service_name = ENV['OTEL_SERVICE_NAME'] || ENV['AWS_LAMBDA_FUNCTION_NAME'] || resource_attributes['service.name'] |
| 52 | + end |
| 53 | + |
| 54 | + ENV['OTEL_SERVICE_NAME'] = @service_name |
| 55 | + end |
| 56 | + |
| 57 | + def mask_token(token) |
| 58 | + token = token.to_s |
| 59 | + return '*' * token.length if token.length <= 4 |
| 60 | + |
| 61 | + "#{token[0, 2]}#{'*' * (token.length - 4)}#{token[-2, 2]}" |
| 62 | + end |
| 63 | + |
| 64 | + def config_token |
| 65 | + agent_enable = true |
| 66 | + return agent_enable if @localhost |
| 67 | + |
| 68 | + if @lambda_env |
| 69 | + # for case 10 and 11, lambda only care about SW_APM_API_TOKEN, not SW_APM_SERVICE_KEY |
| 70 | + agent_enable = ENV['SW_APM_API_TOKEN'].nil? ? false : true |
| 71 | + else |
| 72 | + |
| 73 | + if ENV['OTEL_EXPORTER_OTLP_METRICS_HEADERS'] |
| 74 | + token_type = 'metrics_token' |
| 75 | + elsif ENV['OTEL_EXPORTER_OTLP_HEADERS'] |
| 76 | + token_type = 'general_token' |
| 77 | + elsif ENV['SW_APM_SERVICE_KEY'] |
| 78 | + token_type = 'service_key' |
| 79 | + else |
| 80 | + token_type = 'invalid' |
| 81 | + end |
| 82 | + |
| 83 | + case token_type |
| 84 | + when 'metrics_token' || 'general_token' |
| 85 | + # exporter header is ok, but still need extract it for sampler http get setting |
| 86 | + headers = token_type == 'general_token' ? ENV['OTEL_EXPORTER_OTLP_HEADERS'] : ENV['OTEL_EXPORTER_OTLP_METRICS_HEADERS'] |
| 87 | + @token = headers.gsub("authorization=Bearer ", "") |
| 88 | + when 'service_key' |
| 89 | + if valid?(ENV['SW_APM_SERVICE_KEY']) |
| 90 | + @token, @service_name = ENV['SW_APM_SERVICE_KEY'].to_s.split(':') |
| 91 | + else |
| 92 | + SolarWindsAPM.logger.warn { "SW_APM_SERVICE_KEY is invalid: #{mask_token(ENV['SW_APM_SERVICE_KEY'])}" } |
| 93 | + end |
| 94 | + |
| 95 | + ENV['OTEL_EXPORTER_OTLP_HEADERS'] = "authorization=Bearer #{@token}" |
| 96 | + end |
| 97 | + |
| 98 | + agent_enable = token_type == 'invalid' ? false : true |
| 99 | + end |
| 100 | + |
| 101 | + @agent_enable = agent_enable |
| 102 | + agent_enable |
| 103 | + end |
| 104 | + |
| 105 | + def valid?(service_key) |
| 106 | + # servicekey checker also works on service name, may need to remove that part |
| 107 | + service_key_checker = SolarWindsAPM::ServiceKeyChecker.new('ssl', false) |
| 108 | + service_key_name = service_key_checker.read_and_validate_service_key |
| 109 | + service_key_name == '' ? false : true |
| 110 | + end |
| 111 | + |
| 112 | + def determine_lambda_env |
| 113 | + if ENV['LAMBDA_TASK_ROOT'].to_s.empty? && ENV['AWS_LAMBDA_FUNCTION_NAME'].to_s.empty? |
| 114 | + false |
| 115 | + else |
| 116 | + SolarWindsAPM.logger.debug { "[#{self.class}/#{__method__}] lambda environment - LAMBDA_TASK_ROOT: #{ENV.fetch('LAMBDA_TASK_ROOT', nil)}; AWS_LAMBDA_FUNCTION_NAME: #{ENV.fetch('AWS_LAMBDA_FUNCTION_NAME', nil)}" } |
| 117 | + true |
| 118 | + end |
| 119 | + end |
| 120 | + |
| 121 | + # token and endpoint also need to be considered for get settings |
| 122 | + # endpoint is for get settings, populate to OTEL_EXPORTER_OTLP_METRICS_ENDPOINT at the end for otlp related exporter |
| 123 | + # three sources: otlp env variable, sw env variable, sw config file |
| 124 | + |
| 125 | + # sampler_config = { |
| 126 | + # collector: "https://#{ENV.fetch('SW_APM_COLLECTOR', 'apm.collector.na-01.cloud.solarwinds.com')}:443", |
| 127 | + # service: service_key_name[1], |
| 128 | + # headers: "Bearer #{service_key_name[0]}", |
| 129 | + # tracing_mode: SolarWindsAPM::Config[:tracing_mode], |
| 130 | + # trigger_trace_enabled: SolarWindsAPM::Config[:trigger_tracing_mode], |
| 131 | + # transaction_settings: SolarWindsAPM::Config[:transaction_settings] |
| 132 | + # } |
| 133 | + def configure_otlp_endpoint(data_type) |
| 134 | + # for staging, our purpose, just use OTEL_EXPORTER_OTLP_METRICS_ENDPOINT directly |
| 135 | + # https://otel.collector.cloud.solarwinds.com:443/v1/traces |
| 136 | + # SW_ENDPOINT_REGEX = /^apm\.collector(?:\.[a-z0-9-]+)?\.cloud\.solarwinds\.com$/ |
| 137 | + |
| 138 | + return unless ['TRACES','METRICS','LOGS'].include?(data_type) |
| 139 | + |
| 140 | + data_type_upper = data_type.upcase |
| 141 | + data_type = data_type.downcase |
| 142 | + |
| 143 | + endpoint_type = nil |
| 144 | + if ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"] |
| 145 | + endpoint_type = "#{data_type}_endpoint" |
| 146 | + elsif ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] |
| 147 | + endpoint_type = 'general_endpoint' |
| 148 | + elsif ENV['SW_APM_COLLECTOR'].nil? && !@lambda_env |
| 149 | + endpoint_type = 'default_nil' |
| 150 | + elsif ENV['SW_APM_COLLECTOR'].to_s.match?(SW_ENDPOINT_REGEX) |
| 151 | + endpoint_type = 'apm_proto' |
| 152 | + else |
| 153 | + endpoint_type = 'invalid' |
| 154 | + end |
| 155 | + |
| 156 | + # endpoint = nil |
| 157 | + sampler_collector_endpoint = nil |
| 158 | + case endpoint_type |
| 159 | + when "#{data_type}_endpoint" || 'general_endpoint' |
| 160 | + # no need to worry about metrics endpoint, just need to make sure the collector endpoint is set for getsetting |
| 161 | + endpoint = endpoint_type == 'general_endpoint' ? ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] : ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"] |
| 162 | + |
| 163 | + if endpoint.to_s.match?(OTEL_ENDPOINT_REGEX) |
| 164 | + matches = endpoint.to_s.match(OTEL_ENDPOINT_REGEX) |
| 165 | + region = matches[1] |
| 166 | + sampler_collector_endpoint = DEFAULT_APMPROTO_ENDPOINT.gsub('na-01', region) |
| 167 | + ENV['SW_APM_COLLECTOR'] = sampler_collector_endpoint |
| 168 | + else |
| 169 | + # not the standard otel endpoint, use it directly |
| 170 | + # what to do with collector ? |
| 171 | + end |
| 172 | + |
| 173 | + when 'default_nil' |
| 174 | + # default_nil => no otlp endpoint or no SW_APM_COLLECTOR, use the default apm proto endpoint |
| 175 | + ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"] = "#{DEFAULT_OTLP_ENDPOINT}/v1/#{data_type}" |
| 176 | + ENV['SW_APM_COLLECTOR'] = DEFAULT_APMPROTO_ENDPOINT |
| 177 | + |
| 178 | + when 'apm_proto' |
| 179 | + # default => no otlp endpoint but have SW_APM_COLLECTOR, use the endpoint from SW_APM_COLLECTOR |
| 180 | + # when in testing/staging, we need to set both otlp endpoint and SW_APM_COLLECTOR |
| 181 | + matches = ENV['SW_APM_COLLECTOR'].to_s.match(SW_ENDPOINT_REGEX) |
| 182 | + region = matches[1] |
| 183 | + apmproto_endpoint = DEFAULT_APMPROTO_ENDPOINT.gsub("na-01", region) |
| 184 | + apmproto_endpoint = apmproto_endpoint.gsub("apm", "otel") |
| 185 | + ENV["OTEL_EXPORTER_OTLP_#{data_type_upper}_ENDPOINT"] = "https://#{apmproto_endpoint}:443/v1/#{data_type}" |
| 186 | + end |
| 187 | + |
| 188 | + # true means setup ok, false meaning setup failed |
| 189 | + # lambda use collector extension to export, so no need have valid endpoint_type |
| 190 | + endpoint_type == 'invalid' && !@lambda_env ? false : true |
| 191 | + end |
| 192 | + end |
| 193 | +end |
0 commit comments