1212import org .apache .commons .lang3 .StringUtils ;
1313import org .eclipse .openvsx .entities .Extension ;
1414import org .eclipse .openvsx .entities .FileResource ;
15+ import org .eclipse .openvsx .migration .HandlerJobRequest ;
1516import org .eclipse .openvsx .storage .AwsStorageService ;
1617import org .eclipse .openvsx .util .TempFile ;
1718import org .jobrunr .jobs .annotations .Job ;
18- import org .jobrunr .jobs .annotations . Recurring ;
19+ import org .jobrunr .jobs .lambdas . JobRequestHandler ;
1920import org .slf4j .Logger ;
2021import org .slf4j .LoggerFactory ;
2122import org .springframework .beans .factory .annotation .Value ;
2223import org .springframework .stereotype .Component ;
2324import org .springframework .util .StopWatch ;
2425import org .springframework .web .util .UriUtils ;
25- import software .amazon .awssdk .auth .credentials .*;
2626import software .amazon .awssdk .core .sync .ResponseTransformer ;
2727import software .amazon .awssdk .services .s3 .S3Client ;
2828import software .amazon .awssdk .services .s3 .model .*;
2929
30+ import javax .annotation .PostConstruct ;
3031import java .io .*;
3132import java .nio .charset .StandardCharsets ;
3233import java .nio .file .Files ;
4142/**
4243 * Pulls logs from an Amazon S3 bucket, extracts downloads from the logs and updates download counts in the database.
4344 * <p>
44- * Currently only log files uploaded by Amazon CloudFront are supported.
45+ * The following log file formats are supported:
46+ * <ul>
47+ * <li>cloudfront</li>
48+ * <li>fastly</li>
49+ * </ul>
4550 * <p>
46- * Links:
51+ * See
4752 * <ul>
48- * <li><a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/standard-logging.html">CloudFront standard logging</a></li>
49- * <li><a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/standard-logs-reference.html">CloudFront log format</a></li>
53+ * <li><a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/standard-logging.html">CloudFront standard logging</a></li>
54+ * <li><a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/standard-logs-reference.html">CloudFront log format</a></li>
55+ * <li><a href="https://www.fastly.com/documentation/guides/integrations/streaming-logs/custom-log-formats/">Fastly custom log format</a></li>
5056 * </ul>
5157 */
5258@ Component
53- public class AwsDownloadCountService {
54- private final Logger logger = LoggerFactory .getLogger (AwsDownloadCountService .class );
59+ public class AwsDownloadCountHandler implements JobRequestHandler < HandlerJobRequest <?>> {
60+ private final Logger logger = LoggerFactory .getLogger (AwsDownloadCountHandler .class );
5561
5662 private static final String LOG_LOCATION_PREFIX = "AWSLogs/" ;
5763 private static final int MAX_KEYS = 100 ;
@@ -65,11 +71,36 @@ public class AwsDownloadCountService {
6571 @ Value ("${ovsx.logs.aws.log-location-prefix:" + LOG_LOCATION_PREFIX + "}" )
6672 String logLocationPrefix ;
6773
68- public AwsDownloadCountService (AwsStorageService awsStorageService , DownloadCountProcessor processor ) {
74+ @ Value ("${ovsx.logs.aws.format:cloudfront}" )
75+ String logFormat ;
76+
77+ @ Value ("${ovsx.logs.aws.cron:0 10 * * * *}" )
78+ String cronSchedule ;
79+
80+ LogFileParser logFileParser ;
81+
82+ public AwsDownloadCountHandler (AwsStorageService awsStorageService , DownloadCountProcessor processor ) {
6983 this .awsStorageService = awsStorageService ;
7084 this .processor = processor ;
7185 }
7286
87+ @ PostConstruct
88+ public void initialize () {
89+ logFileParser = switch (logFormat .toLowerCase ()) {
90+ case "cloudfront" -> new CloudFrontLogFileParser ();
91+ case "fastly" -> new FastlyLogFileParser ();
92+ default -> throw new IllegalArgumentException ("unsupported log file format '" + logFormat + "'" );
93+ };
94+ }
95+
96+ public String getRecurringJobId () {
97+ return "update-aws-download-counts" ;
98+ }
99+
100+ public String getCronSchedule () {
101+ return cronSchedule ;
102+ }
103+
73104 /**
74105 * Indicates whether the download service is enabled by application config.
75106 */
@@ -82,11 +113,11 @@ private S3Client getS3Client() {
82113 }
83114
84115 /**
85- * Task scheduled once per hour to pull logs from AWS S3 Storage and update extension download counts.
116+ * Scheduled task to pull logs from AWS S3 Storage and update extension download counts.
86117 */
118+ @ Override
87119 @ Job (name = "Update AWS Download Counts" , retries = 0 )
88- @ Recurring (id = "update-aws-download-counts" , cron = "0 10 * * * *" , zoneId = "UTC" )
89- public void updateDownloadCounts () {
120+ public void run (HandlerJobRequest <?> jobRequest ) throws Exception {
90121 if (!isEnabled ()) {
91122 return ;
92123 }
@@ -193,16 +224,14 @@ private Map<String, Integer> processLogFile(String fileName) throws IOException
193224 var lines = reader .lines ().iterator ();
194225 while (lines .hasNext ()) {
195226 var line = lines .next ();
196- if (line .startsWith ("#" )) {
227+
228+ var record = logFileParser .parse (line );
229+ if (record == null ) {
197230 continue ;
198231 }
199232
200- // Format:
201- // date time x-edge-location sc-bytes c-ip cs-method cs(Host) cs-uri-stem sc-status cs(Referer) cs(User-Agent) cs-uri-query cs(Cookie) x-edge-result-type x-edge-request-id x-host-header cs-protocol cs-bytes time-taken x-forwarded-for ssl-protocol ssl-cipher x-edge-response-result-type cs-protocol-version fle-status fle-encrypted-fields c-port time-to-first-byte x-edge-detailed-result-type sc-content-type sc-content-len sc-range-start sc-range-end
202- var components = line .split ("[ \t ]+" );
203-
204- if (isGetOperation (components ) && isStatusOk (components ) && isExtensionPackageUri (components )) {
205- var uri = components [7 ];
233+ if (isGetOperation (record ) && isStatusOk (record ) && isExtensionPackageUri (record )) {
234+ var uri = record .url ();
206235 var uriComponents = uri .split ("/" );
207236 var vsixFile = UriUtils .decode (uriComponents [uriComponents .length - 1 ], StandardCharsets .UTF_8 ).toUpperCase ();
208237 fileCounts .merge (vsixFile , 1 , Integer ::sum );
@@ -212,16 +241,16 @@ private Map<String, Integer> processLogFile(String fileName) throws IOException
212241 }
213242 }
214243
215- private boolean isGetOperation (String [] components ) {
216- return components [ 5 ] .equalsIgnoreCase ("GET" );
244+ private boolean isGetOperation (LogRecord record ) {
245+ return record . method () .equalsIgnoreCase ("GET" );
217246 }
218247
219- private boolean isStatusOk (String [] components ) {
220- return Integer . parseInt ( components [ 8 ] ) == 200 ;
248+ private boolean isStatusOk (LogRecord record ) {
249+ return record . status ( ) == 200 ;
221250 }
222251
223- private boolean isExtensionPackageUri (String [] components ) {
224- return components [ 7 ] .endsWith (".vsix" );
252+ private boolean isExtensionPackageUri (LogRecord record ) {
253+ return record . url () .endsWith (".vsix" );
225254 }
226255
227256 private TempFile downloadFile (String objectKey ) throws IOException {
0 commit comments