Skip to content

Commit 165918c

Browse files
committed
Fix #34306: add CSV usage stats backend
1 parent 399e330 commit 165918c

5 files changed

Lines changed: 174 additions & 37 deletions

File tree

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,10 @@
352352
<groupId>com.fasterxml.jackson.datatype</groupId>
353353
<artifactId>jackson-datatype-jsr353</artifactId>
354354
</dependency>
355+
<dependency>
356+
<groupId>com.fasterxml.jackson.dataformat</groupId>
357+
<artifactId>jackson-dataformat-csv</artifactId>
358+
</dependency>
355359
<dependency>
356360
<groupId>org.projectlombok</groupId>
357361
<artifactId>lombok</artifactId>

src/main/java/eu/openanalytics/containerproxy/stat/StatCollectorFactory.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import eu.openanalytics.containerproxy.spec.expression.SpecExpressionContext;
2525
import eu.openanalytics.containerproxy.spec.expression.SpecExpressionResolver;
2626
import eu.openanalytics.containerproxy.spec.expression.SpelField;
27+
import eu.openanalytics.containerproxy.stat.impl.CSVCollector;
2728
import eu.openanalytics.containerproxy.stat.impl.InfluxDBCollector;
2829
import eu.openanalytics.containerproxy.stat.impl.JDBCCollector;
2930
import eu.openanalytics.containerproxy.stat.impl.Micrometer;
@@ -120,6 +121,8 @@ private IStatCollector createCollector(String url, String username, String passw
120121
} else if (url.equalsIgnoreCase("micrometer")) {
121122
createBean(new ProxySharingMicrometer(), "ProxySharingMicrometer");
122123
return new Micrometer();
124+
} else if (url.toLowerCase().endsWith(".csv")) {
125+
return new CSVCollector(url, usageStatsAttributes);
123126
} else {
124127
throw new IllegalArgumentException(String.format("Base url for statistics contains an unrecognized values, baseURL %s.", url));
125128
}

src/main/java/eu/openanalytics/containerproxy/stat/impl/AbstractDbCollector.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,28 @@
2626
import eu.openanalytics.containerproxy.event.ProxyStopEvent;
2727
import eu.openanalytics.containerproxy.event.UserLoginEvent;
2828
import eu.openanalytics.containerproxy.event.UserLogoutEvent;
29+
import eu.openanalytics.containerproxy.spec.expression.SpecExpressionContext;
30+
import eu.openanalytics.containerproxy.spec.expression.SpecExpressionResolver;
2931
import eu.openanalytics.containerproxy.stat.IStatCollector;
32+
import eu.openanalytics.containerproxy.stat.StatCollectorFactory;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
3035
import org.springframework.context.ApplicationEvent;
3136
import org.springframework.context.event.EventListener;
3237
import org.springframework.security.core.Authentication;
3338

39+
import javax.inject.Inject;
3440
import java.io.IOException;
41+
import java.util.HashMap;
42+
import java.util.List;
43+
import java.util.Map;
3544

3645
public abstract class AbstractDbCollector implements IStatCollector {
3746

47+
@Inject
48+
private SpecExpressionResolver specExpressionResolver;
49+
private final Logger logger = LoggerFactory.getLogger(getClass());
50+
3851
@EventListener
3952
public void onUserLogoutEvent(UserLogoutEvent event) throws IOException {
4053
writeToDb(event, event.getTimestamp(), event.getUserId(), "Logout", null, event.getAuthentication());
@@ -71,4 +84,27 @@ public void onAuthFailedEvent(AuthFailedEvent event) {
7184

7285
protected abstract void writeToDb(ApplicationEvent event, long timestamp, String userId, String type, String data, Authentication authentication) throws IOException;
7386

87+
protected Map<String, String> resolveAttributes(Authentication authentication, ApplicationEvent event, List<StatCollectorFactory.UsageStatsAttribute> usageStatsAttributes) {
88+
if (usageStatsAttributes == null) {
89+
return new HashMap<>();
90+
}
91+
SpecExpressionContext context;
92+
if (authentication != null) {
93+
context = SpecExpressionContext.create(authentication, authentication.getPrincipal(), authentication.getCredentials(), event);
94+
} else {
95+
context = SpecExpressionContext.create(event);
96+
}
97+
98+
Map<String, String> result = new HashMap<>();
99+
100+
for (StatCollectorFactory.UsageStatsAttribute attribute : usageStatsAttributes) {
101+
try {
102+
result.put(attribute.getName(), specExpressionResolver.evaluateToString(attribute.getExpression(), context));
103+
} catch (Exception e) {
104+
logger.warn("Error while resolving attribute expression '{}'", attribute.getName(), e);
105+
}
106+
}
107+
return result;
108+
}
109+
74110
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2024 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.stat.impl;
22+
23+
import com.fasterxml.jackson.databind.MappingIterator;
24+
import com.fasterxml.jackson.databind.SequenceWriter;
25+
import com.fasterxml.jackson.dataformat.csv.CsvGenerator;
26+
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
27+
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
28+
import eu.openanalytics.containerproxy.stat.StatCollectorFactory;
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
31+
import org.springframework.context.ApplicationEvent;
32+
import org.springframework.security.core.Authentication;
33+
34+
import javax.annotation.PostConstruct;
35+
import java.io.FileWriter;
36+
import java.io.IOException;
37+
import java.nio.file.Files;
38+
import java.nio.file.Path;
39+
import java.util.HashMap;
40+
import java.util.List;
41+
import java.util.Map;
42+
import java.util.Objects;
43+
import java.util.UUID;
44+
45+
public class CSVCollector extends AbstractDbCollector implements AutoCloseable {
46+
47+
private final Path url;
48+
private final List<StatCollectorFactory.UsageStatsAttribute> usageStatsAttributes;
49+
private final Logger logger = LoggerFactory.getLogger(getClass());
50+
private FileWriter fileWriter;
51+
private SequenceWriter writer;
52+
private CsvSchema schema;
53+
54+
public CSVCollector(String url, List<StatCollectorFactory.UsageStatsAttribute> usageStatsAttributes) {
55+
this.url = Path.of(url);
56+
this.usageStatsAttributes = usageStatsAttributes;
57+
}
58+
59+
@PostConstruct
60+
public void init() throws IOException {
61+
CsvMapper csvMapper = new CsvMapper();
62+
csvMapper.enable(CsvGenerator.Feature.ALWAYS_QUOTE_STRINGS);
63+
csvMapper.enable(CsvGenerator.Feature.ALWAYS_QUOTE_EMPTY_STRINGS);
64+
CsvSchema.Builder schemaBuilder = CsvSchema.builder();
65+
if (Files.exists(url) && Files.size(url) > 0) {
66+
CsvSchema csvSchema = csvMapper.typedSchemaFor(Map.class).withHeader();
67+
try {
68+
try (MappingIterator<Map<String, String>> it = csvMapper.readerFor(Map.class)
69+
.with(csvSchema.withColumnSeparator(','))
70+
.readValues(url.toFile())) {
71+
for (String existingColumn : it.next().keySet()) {
72+
schemaBuilder.addColumn(existingColumn);
73+
}
74+
}
75+
fileWriter = new FileWriter(url.toFile(), true);
76+
} catch (Exception e) {
77+
String newUrl = url.toString().replace(".csv", "-" + UUID.randomUUID() + ".csv");
78+
logger.warn("Not re-using existing csv file for usage stats (not in expected format), writing to {}", newUrl, e);
79+
fileWriter = new FileWriter(newUrl);
80+
schemaBuilder.setUseHeader(true);
81+
}
82+
} else {
83+
fileWriter = new FileWriter(url.toFile());
84+
schemaBuilder.setUseHeader(true);
85+
}
86+
if (!schemaBuilder.hasColumn("event_time")) {
87+
schemaBuilder.addColumn("event_time");
88+
}
89+
if (!schemaBuilder.hasColumn("username")) {
90+
schemaBuilder.addColumn("username");
91+
}
92+
if (!schemaBuilder.hasColumn("type")) {
93+
schemaBuilder.addColumn("type");
94+
}
95+
if (!schemaBuilder.hasColumn("data")) {
96+
schemaBuilder.addColumn("data");
97+
}
98+
99+
if (usageStatsAttributes != null) {
100+
for (StatCollectorFactory.UsageStatsAttribute attribute : usageStatsAttributes) {
101+
if (!schemaBuilder.hasColumn(attribute.getName())) {
102+
schemaBuilder.addColumn(attribute.getName());
103+
}
104+
}
105+
}
106+
107+
schema = schemaBuilder.build();
108+
writer = csvMapper.writer(schema).writeValues(fileWriter);
109+
}
110+
111+
@Override
112+
protected void writeToDb(ApplicationEvent event, long timestamp, String userId, String type, String data, Authentication authentication) throws IOException {
113+
Map<String, String> row = new HashMap<>();
114+
for (String column : schema.getColumnNames()) {
115+
row.put(column, "");
116+
}
117+
row.put("event_time", Long.toString(timestamp));
118+
row.put("username", Objects.requireNonNullElse(userId, ""));
119+
row.put("type", Objects.requireNonNullElse(type, ""));
120+
row.put("data", Objects.requireNonNullElse(data, ""));
121+
row.putAll(resolveAttributes(authentication, event, usageStatsAttributes));
122+
writer.write(row);
123+
}
124+
125+
@Override
126+
public void close() throws Exception {
127+
writer.close();
128+
fileWriter.close();
129+
}
130+
}

src/main/java/eu/openanalytics/containerproxy/stat/impl/JDBCCollector.java

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@
2121
package eu.openanalytics.containerproxy.stat.impl;
2222

2323
import com.zaxxer.hikari.HikariDataSource;
24-
import eu.openanalytics.containerproxy.spec.expression.SpecExpressionContext;
25-
import eu.openanalytics.containerproxy.spec.expression.SpecExpressionResolver;
2624
import eu.openanalytics.containerproxy.stat.StatCollectorFactory;
27-
import org.slf4j.Logger;
28-
import org.slf4j.LoggerFactory;
2925
import org.springframework.context.ApplicationEvent;
3026
import org.springframework.core.env.Environment;
3127
import org.springframework.security.core.Authentication;
@@ -40,7 +36,6 @@
4036
import java.sql.SQLException;
4137
import java.sql.Statement;
4238
import java.sql.Timestamp;
43-
import java.util.HashMap;
4439
import java.util.List;
4540
import java.util.Map;
4641
import java.util.Set;
@@ -77,11 +72,6 @@ public class JDBCCollector extends AbstractDbCollector {
7772
@Inject
7873
private Environment environment;
7974

80-
@Inject
81-
private SpecExpressionResolver specExpressionResolver;
82-
83-
private Logger logger = LoggerFactory.getLogger(getClass());
84-
8575
public JDBCCollector(String url, String username, String password, String tableName, List<StatCollectorFactory.UsageStatsAttribute> usageStatsAttributes) {
8676
this.url = url;
8777
this.username = username;
@@ -159,7 +149,7 @@ public void init() throws IOException {
159149
@Override
160150
protected void writeToDb(ApplicationEvent event, long timestamp, String userId, String type, String data, Authentication authentication) throws IOException {
161151
try (Connection con = ds.getConnection()) {
162-
Map<String, String> attributes = resolveAttributes(authentication, event);
152+
Map<String, String> attributes = resolveAttributes(authentication, event, usageStatsAttributes);
163153
String sql = buildQuery(attributes.keySet());
164154

165155
try (PreparedStatement stmt = con.prepareStatement(sql)) {
@@ -177,8 +167,6 @@ protected void writeToDb(ApplicationEvent event, long timestamp, String userId,
177167
}
178168
} catch (SQLException e) {
179169
throw new IOException("Exception while logging stats", e);
180-
} catch (Exception e) {
181-
logger.error("oops", e); //TODO
182170
}
183171
}
184172

@@ -198,28 +186,4 @@ private String buildQuery(Set<String> columns) {
198186
sql.append(")");
199187
return sql.toString();
200188
}
201-
202-
private Map<String, String> resolveAttributes(Authentication authentication, ApplicationEvent event) {
203-
if (usageStatsAttributes == null) {
204-
return new HashMap<>();
205-
}
206-
SpecExpressionContext context;
207-
if (authentication != null) {
208-
context = SpecExpressionContext.create(authentication, authentication.getPrincipal(), authentication.getCredentials(), event);
209-
} else {
210-
context = SpecExpressionContext.create(event);
211-
}
212-
213-
Map<String, String> result = new HashMap<>();
214-
215-
for (StatCollectorFactory.UsageStatsAttribute attribute : usageStatsAttributes) {
216-
try {
217-
result.put(attribute.getName(), specExpressionResolver.evaluateToString(attribute.getExpression(), context));
218-
} catch (Exception e) {
219-
logger.warn("Error while resolving attribute expression '{}'", attribute.getName(), e);
220-
}
221-
}
222-
return result;
223-
}
224-
225189
}

0 commit comments

Comments
 (0)