Skip to content

Commit 0d4a0e9

Browse files
authored
Merge pull request #599 from govuk-one-login/enhance-clean
feat(EventProbe): error handling + enhance clean
2 parents 228d2bf + 7b06dc6 commit 0d4a0e9

3 files changed

Lines changed: 163 additions & 10 deletions

File tree

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ plugins {
1111
}
1212

1313
// Important: see README on publishing a new version to Maven
14-
def buildVersion = "8.0.1"
14+
def buildVersion = "8.0.2"
1515

1616
defaultTasks 'clean', 'spotlessApply', 'build'
1717

src/main/java/uk/gov/di/ipv/cri/common/library/util/EventProbe.java

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,21 @@ private void logErrorCause(Throwable throwable) {
5050
}
5151

5252
public EventProbe counterMetric(String key) {
53-
metrics.addMetric(key, 1d);
53+
addMetric(key, 1d);
5454
return this;
5555
}
5656

5757
public EventProbe counterMetric(String key, double value) {
58-
metrics.addMetric(key, value);
58+
addMetric(key, value);
5959
return this;
6060
}
6161

6262
public EventProbe counterMetric(String key, double value, MetricUnit unit) {
63-
metrics.addMetric(key, value, unit);
63+
try {
64+
metrics.addMetric(key, value, unit);
65+
} catch (Exception e) {
66+
LOGGER.error("Counter metric failed", e);
67+
}
6468
return this;
6569
}
6670

@@ -83,16 +87,86 @@ public EventProbe addJourneyIdToLoggingContext(String journeyId) {
8387

8488
public void addDimensions(Map<String, String> dimensions) {
8589
if (dimensions != null) {
86-
DimensionSet dimensionSet = new DimensionSet();
87-
dimensions.forEach(dimensionSet::addDimension);
88-
metrics.addDimension(dimensionSet);
90+
DimensionSet dimensionSet = newDimensionSet();
91+
dimensions.forEach(
92+
(key, value) -> {
93+
try {
94+
dimensionSet.addDimension(key, value);
95+
} catch (Exception e) {
96+
LOGGER.error("Metric failed validation: {}={}", key, value, e);
97+
}
98+
});
99+
try {
100+
metrics.addDimension(dimensionSet);
101+
} catch (Exception e) {
102+
LOGGER.error("Failed to add dimensions", e);
103+
}
104+
}
105+
}
106+
107+
private void addMetric(String key, double value) {
108+
try {
109+
metrics.addMetric(key, value);
110+
} catch (Exception e) {
111+
LOGGER.error("Failed to add metric: {}, trying again with clean", key, e);
89112
}
90113
}
91114

92-
public static String clean(String metricValue) {
93-
if (metricValue == null || metricValue.isBlank()) {
115+
public static String clean(String value) {
116+
final int maxLen = 250;
117+
118+
if (value == null || value.isBlank()) {
119+
return "no_content";
120+
}
121+
122+
String stripped = removePrefixedColons(value);
123+
char[] chars = stripped.toCharArray();
124+
125+
if (chars.length == 0 || stripped.isBlank()) {
94126
return "no_content";
95127
}
96-
return metricValue.replaceAll("\\s+", "_").trim();
128+
129+
StringBuilder sb = new StringBuilder();
130+
131+
for (char c : chars) {
132+
if (Character.isWhitespace(c)) {
133+
sb.append("_");
134+
} else if (isAsciiPrintable(c)) {
135+
sb.append(c);
136+
} else {
137+
sb.append("_");
138+
}
139+
}
140+
141+
String cleaned = sb.toString();
142+
143+
if (cleaned.length() > maxLen) {
144+
cleaned = cleaned.substring(0, maxLen);
145+
}
146+
147+
String result = cleaned.replaceAll("\\s+", "_").trim();
148+
if (!result.equalsIgnoreCase(value)) {
149+
LOGGER.warn(
150+
"Dimension value of {} has been transformed into {} to match Powertools validations",
151+
value,
152+
result);
153+
}
154+
return result;
155+
}
156+
157+
private static String removePrefixedColons(String value) {
158+
if (value.startsWith(":")) {
159+
return removePrefixedColons(value.substring(1));
160+
} else {
161+
return value;
162+
}
163+
}
164+
165+
private static boolean isAsciiPrintable(final char ch) {
166+
return ch >= 32 && ch < 127;
167+
}
168+
169+
public DimensionSet newDimensionSet() {
170+
return new DimensionSet();
97171
}
98172
}

src/test/java/uk/gov/di/ipv/cri/common/library/util/EventProbeTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,28 @@
1414
import org.mockito.junit.jupiter.MockitoExtension;
1515
import org.slf4j.MDC;
1616
import software.amazon.lambda.powertools.metrics.Metrics;
17+
import software.amazon.lambda.powertools.metrics.internal.Validator;
1718
import software.amazon.lambda.powertools.metrics.model.DimensionSet;
1819
import software.amazon.lambda.powertools.metrics.model.MetricUnit;
1920

2021
import java.util.Map;
2122

2223
import static org.hamcrest.MatcherAssert.assertThat;
2324
import static org.hamcrest.Matchers.equalTo;
25+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
2426
import static org.junit.jupiter.api.Assertions.assertEquals;
2527
import static org.junit.jupiter.api.Assertions.assertFalse;
2628
import static org.junit.jupiter.api.Assertions.assertNotNull;
2729
import static org.junit.jupiter.api.Assertions.assertNull;
2830
import static org.junit.jupiter.api.Assertions.assertSame;
2931
import static org.junit.jupiter.api.Assertions.assertTrue;
3032
import static org.mockito.ArgumentMatchers.any;
33+
import static org.mockito.ArgumentMatchers.anyString;
34+
import static org.mockito.Mockito.doReturn;
35+
import static org.mockito.Mockito.doThrow;
36+
import static org.mockito.Mockito.mock;
3137
import static org.mockito.Mockito.never;
38+
import static org.mockito.Mockito.spy;
3239
import static org.mockito.Mockito.verify;
3340

3441
@ExtendWith(MockitoExtension.class)
@@ -203,7 +210,79 @@ void shouldCleanMetricDimension() {
203210
assertEquals("this_has_whitespace", EventProbe.clean("this has whitespace"));
204211
assertEquals("correct_value", EventProbe.clean("correct_value"));
205212
assertEquals("no_content", EventProbe.clean(""));
213+
assertEquals("no_content", EventProbe.clean(" "));
206214
assertEquals("no_content", EventProbe.clean(null));
207215
assertEquals("test_test", EventProbe.clean("test\ntest"));
216+
assertEquals("test", EventProbe.clean(":test"));
217+
assertEquals("test", EventProbe.clean("::test"));
218+
assertEquals("no_content", EventProbe.clean(":"));
219+
assertEquals(250, EventProbe.clean("1".repeat(300)).length());
220+
assertEquals(
221+
"includes_non_ascii_printable___char",
222+
EventProbe.clean("includes_non_ascii_printable_¬_char"));
223+
}
224+
225+
@Test
226+
void shouldCleanAComplexString() {
227+
String complexString =
228+
EventProbe.clean(
229+
"""
230+
:Hello world\t😀
231+
éñö @@##!! 12345 \f \\slashes// ""quotes""
232+
line2-with-stuff 🧨🚀✨
233+
more-text---here---###$$$%%%&&&
234+
control-\u0007-char
235+
𝔘𝔫𝔦𝔠𝔬𝔡𝔢 block
236+
final-line-with: symbols_*&^%$#@!+=()[]{}<>?/|~`
237+
😀😀 end
238+
"""
239+
.repeat(500));
240+
241+
assertDoesNotThrow(() -> Validator.validateDimension("_", complexString));
242+
assertEquals(250, complexString.length());
243+
}
244+
245+
@Test
246+
void shouldFallbackOnBadMetric() {
247+
assertDoesNotThrow(() -> eventProbe.counterMetric("with space"));
248+
assertDoesNotThrow(() -> eventProbe.counterMetric("with space", 1d));
249+
}
250+
251+
@Test
252+
void counterMetricHandlesExceptionGracefully() {
253+
doThrow(new RuntimeException("error")).when(mockMetrics).addMetric("bad metric", 1d);
254+
255+
EventProbe result = assertDoesNotThrow(() -> eventProbe.counterMetric("bad-metric"));
256+
257+
verify(mockMetrics).addMetric("bad-metric", 1d);
258+
assertSame(eventProbe, result);
259+
}
260+
261+
@Test
262+
void counterMetricWithValueHandlesExceptionGracefully() {
263+
doThrow(new RuntimeException("error")).when(mockMetrics).addMetric("bad-metric-2", 42d);
264+
265+
EventProbe result = assertDoesNotThrow(() -> eventProbe.counterMetric("bad-metric-2", 42d));
266+
267+
verify(mockMetrics).addMetric("bad-metric-2", 42d);
268+
assertSame(eventProbe, result);
269+
}
270+
271+
@Test
272+
void addDimensionsHandlesExceptionsGracefully() {
273+
DimensionSet mockSet = mock(DimensionSet.class);
274+
275+
doThrow(new RuntimeException("error")).when(mockSet).addDimension(anyString(), anyString());
276+
277+
EventProbe spyProbe = spy(new EventProbe(mockMetrics));
278+
doReturn(mockSet).when(spyProbe).newDimensionSet();
279+
280+
doThrow(new RuntimeException("error"))
281+
.when(mockMetrics)
282+
.addDimension(any(DimensionSet.class));
283+
284+
assertDoesNotThrow(() -> spyProbe.addDimensions(Map.of("bad Key", "bad Value")));
285+
286+
verify(mockMetrics).addDimension(mockSet);
208287
}
209288
}

0 commit comments

Comments
 (0)