-
Notifications
You must be signed in to change notification settings - Fork 28
Chargebee telemetry handling #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| /* | ||
| * Copyright 2026 Chargebee Inc. | ||
| */ | ||
|
|
||
| package com.chargebee.v4.telemetry; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| /** Parses the {@code X-Chargebee-Telemetry} response header into OpenTelemetry span attributes. */ | ||
| public final class ChargebeeTelemetryHeaderParser { | ||
|
|
||
| static final String SF_DATE_PREFIX = "@"; | ||
| static final String SF_BOOLEAN_TRUE = "?1"; | ||
| static final String SF_BOOLEAN_FALSE = "?0"; | ||
|
|
||
| private static final Pattern INTEGER_PATTERN = Pattern.compile("-?\\d+"); | ||
| private static final Pattern DECIMAL_PATTERN = Pattern.compile("-?\\d+\\.\\d+"); | ||
|
|
||
| private ChargebeeTelemetryHeaderParser() { | ||
| // utility class | ||
| } | ||
|
|
||
| /** | ||
| * Parses a raw {@code X-Chargebee-Telemetry} header value into typed span attributes. | ||
| * | ||
| * @param headerValue raw header string | ||
| * @return parsed attributes, or empty map when input is null/blank or structurally invalid | ||
| */ | ||
| public static Map<String, Object> parseToSpanAttributes(String headerValue) { | ||
| if (headerValue == null || headerValue.trim().isEmpty()) { | ||
| return Collections.emptyMap(); | ||
| } | ||
|
|
||
| try { | ||
| Map<String, Object> attributes = new HashMap<>(); | ||
| List<String> features = new ArrayList<>(); | ||
|
|
||
| for (String item : splitListItems(headerValue)) { | ||
| parseListItem(item, attributes, features); | ||
| } | ||
|
|
||
| if (!features.isEmpty()) { | ||
| attributes.put(TelemetryAttributeKeys.CHARGEBEE_TELEMETRY_FEATURES, features); | ||
| } | ||
|
|
||
| return attributes.isEmpty() ? Collections.emptyMap() : attributes; | ||
| } catch (RuntimeException ex) { | ||
| return Collections.emptyMap(); | ||
| } | ||
| } | ||
|
|
||
| private static void parseListItem( | ||
| String item, Map<String, Object> attributes, List<String> features) { | ||
| String trimmed = item.trim(); | ||
| if (trimmed.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| int separator = indexOfParameterSeparator(trimmed); | ||
| String token = separator < 0 ? trimmed : trimmed.substring(0, separator).trim(); | ||
| if (token.isEmpty()) { | ||
| throw new IllegalArgumentException("missing sf-item token"); | ||
| } | ||
|
|
||
| if (token.startsWith(TelemetryAttributeKeys.CHARGEBEE_TELEMETRY_FT_PREFIX)) { | ||
| features.add(token.substring(TelemetryAttributeKeys.CHARGEBEE_TELEMETRY_FT_PREFIX.length())); | ||
| return; | ||
| } | ||
|
|
||
| String attributePrefix = segmentAttributePrefix(token); | ||
| if (separator >= 0) { | ||
| parseParameters(trimmed.substring(separator + 1), attributePrefix, attributes); | ||
| } | ||
| } | ||
|
|
||
| private static String segmentAttributePrefix(String token) { | ||
| if (TelemetryAttributeKeys.CHARGEBEE_TELEMETRY_CB_SEGMENT.equals(token)) { | ||
| return TelemetryAttributeKeys.CHARGEBEE_TELEMETRY_CB_PREFIX; | ||
| } | ||
| if (token.startsWith(TelemetryAttributeKeys.CHARGEBEE_TELEMETRY_TP_PREFIX)) { | ||
| return TelemetryAttributeKeys.CHARGEBEE_TELEMETRY_TP_ATTRIBUTE_PREFIX | ||
| + token.substring(TelemetryAttributeKeys.CHARGEBEE_TELEMETRY_TP_PREFIX.length()) | ||
| + "."; | ||
| } | ||
| return TelemetryAttributeKeys.CHARGEBEE_TELEMETRY_PREFIX + token + "."; | ||
| } | ||
|
|
||
| private static void parseParameters( | ||
| String parametersSection, String attributePrefix, Map<String, Object> attributes) { | ||
| for (String parameter : splitParameters(parametersSection)) { | ||
| parseParameter(parameter, attributePrefix, attributes); | ||
| } | ||
| } | ||
|
|
||
| private static void parseParameter( | ||
| String parameter, String attributePrefix, Map<String, Object> attributes) { | ||
| String trimmed = parameter.trim(); | ||
| if (trimmed.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| int equalsIndex = indexOfEquals(trimmed); | ||
| if (equalsIndex <= 0) { | ||
| throw new IllegalArgumentException("invalid parameter: " + trimmed); | ||
| } | ||
|
|
||
| String key = trimmed.substring(0, equalsIndex).trim(); | ||
| String rawValue = trimmed.substring(equalsIndex + 1).trim(); | ||
| if (key.isEmpty()) { | ||
| throw new IllegalArgumentException("missing parameter key"); | ||
| } | ||
|
|
||
| attributes.put(attributePrefix + key, parseScalarValue(rawValue)); | ||
| } | ||
|
|
||
| static Object parseScalarValue(String rawValue) { | ||
| if (rawValue == null || rawValue.isEmpty()) { | ||
| throw new IllegalArgumentException("missing scalar value"); | ||
| } | ||
|
|
||
| if (rawValue.startsWith(SF_DATE_PREFIX)) { | ||
| return Long.parseLong(rawValue.substring(SF_DATE_PREFIX.length())); | ||
| } | ||
| if (SF_BOOLEAN_TRUE.equals(rawValue)) { | ||
| return Boolean.TRUE; | ||
| } | ||
| if (SF_BOOLEAN_FALSE.equals(rawValue)) { | ||
| return Boolean.FALSE; | ||
| } | ||
| if (rawValue.startsWith(":") && rawValue.endsWith(":") && rawValue.length() >= 2) { | ||
| return rawValue.substring(1, rawValue.length() - 1); | ||
| } | ||
| if (rawValue.startsWith("\"")) { | ||
| return parseStringValue(rawValue); | ||
| } | ||
| if (INTEGER_PATTERN.matcher(rawValue).matches()) { | ||
| return Long.parseLong(rawValue); | ||
| } | ||
| if (DECIMAL_PATTERN.matcher(rawValue).matches()) { | ||
| return Double.parseDouble(rawValue); | ||
| } | ||
| return rawValue; | ||
| } | ||
|
|
||
| private static String parseStringValue(String rawValue) { | ||
| if (rawValue.length() < 2 || rawValue.charAt(rawValue.length() - 1) != '"') { | ||
| throw new IllegalArgumentException("invalid sf-string value"); | ||
| } | ||
|
|
||
| StringBuilder decoded = new StringBuilder(); | ||
| for (int i = 1; i < rawValue.length() - 1; i++) { | ||
| char current = rawValue.charAt(i); | ||
| if (current == '\\') { | ||
| if (i + 1 >= rawValue.length() - 1) { | ||
| throw new IllegalArgumentException("invalid sf-string escape"); | ||
| } | ||
| decoded.append(rawValue.charAt(++i)); | ||
| } else { | ||
| decoded.append(current); | ||
| } | ||
| } | ||
| return decoded.toString(); | ||
| } | ||
|
|
||
| private static List<String> splitListItems(String input) { | ||
| return splitOnDelimiter(input, ','); | ||
| } | ||
|
|
||
| private static List<String> splitParameters(String input) { | ||
| return splitOnDelimiter(input, ';'); | ||
| } | ||
|
|
||
| private static List<String> splitOnDelimiter(String input, char delimiter) { | ||
| List<String> parts = new ArrayList<>(); | ||
| StringBuilder current = new StringBuilder(); | ||
| boolean inQuotes = false; | ||
|
|
||
| for (int i = 0; i < input.length(); i++) { | ||
| char currentChar = input.charAt(i); | ||
| if (currentChar == '"') { | ||
| inQuotes = !inQuotes; | ||
| current.append(currentChar); | ||
| } else if (currentChar == delimiter && !inQuotes) { | ||
| addIfNotBlank(parts, current); | ||
| current = new StringBuilder(); | ||
| } else { | ||
| current.append(currentChar); | ||
| } | ||
| } | ||
|
|
||
| addIfNotBlank(parts, current); | ||
| return parts; | ||
| } | ||
|
|
||
| private static int indexOfParameterSeparator(String item) { | ||
| boolean inQuotes = false; | ||
| for (int i = 0; i < item.length(); i++) { | ||
| char current = item.charAt(i); | ||
| if (current == '"') { | ||
| inQuotes = !inQuotes; | ||
| } else if (current == ';' && !inQuotes) { | ||
| return i; | ||
| } | ||
| } | ||
| return -1; | ||
| } | ||
|
|
||
| private static int indexOfEquals(String parameter) { | ||
| boolean inQuotes = false; | ||
| for (int i = 0; i < parameter.length(); i++) { | ||
| char current = parameter.charAt(i); | ||
| if (current == '"') { | ||
| inQuotes = !inQuotes; | ||
| } else if (current == '=' && !inQuotes) { | ||
| return i; | ||
| } | ||
| } | ||
| return -1; | ||
| } | ||
|
Comment on lines
+178
to
+224
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
file='src/main/java/com/chargebee/v4/telemetry/ChargebeeTelemetryHeaderParser.java'
wc -l "$file"
echo '--- outline ---'
ast-grep outline "$file" --view expanded || true
echo '--- relevant lines ---'
sed -n '1,260p' "$file" | cat -nRepository: chargebee/chargebee-java Length of output: 10501 Skip escaped quotes while scanning for delimiters. 🤖 Prompt for AI Agents |
||
|
|
||
| private static void addIfNotBlank(List<String> parts, StringBuilder current) { | ||
| if (current.length() == 0) { | ||
| return; | ||
| } | ||
| String value = current.toString().trim(); | ||
| if (!value.isEmpty()) { | ||
| parts.add(value); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
OpenTelemetry Java Span setAttribute overloads AttributeKey string array List<String> compile💡 Result:
In OpenTelemetry Java, the Span interface uses a generic setAttribute method when working with AttributeKey instances [1][2]. To set a string array (or any list-based attribute), you must use an AttributeKey defined for a List type, such as one created via AttributeKey.stringArrayKey(String) [3][4]. The Span.setAttribute method signature is defined as: Span setAttribute(AttributeKey key, T value); When using this with a string array, the value must be a List, not a raw array [3][5]. Example compilation snippet: import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; import java.util.List; // Define the key for a List of Strings AttributeKey<List> MY_ARRAY_KEY = AttributeKey.stringArrayKey("my.string.array"); // Use the key with the span Span span = Span.current; span.setAttribute(MY_ARRAY_KEY, List.of("value1", "value2")); While there are convenience methods on AttributesBuilder that support varargs (e.g., put(String key, String... values)) [6], the primary Span.setAttribute method using AttributeKey requires the value to match the generic type T exactly—in this case, a List [3][1]. If you have a raw array, you must convert it to a List (e.g., via Arrays.asList or List.of) to satisfy the compiler [7].
Citations:
🏁 Script executed:
Repository: chargebee/chargebee-java
Length of output: 187
Use
AttributeKey.stringArrayKeyfor list attributesSpan.setAttributeonly acceptsAttributeKey<T>for non-scalar values, sospan.setAttribute(k, values)with aStringkey andList<String>value has no matching overload. Use a string-array key here:Fix
📝 Committable suggestion
🤖 Prompt for AI Agents