Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion lib/logstash/filters/useragent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ def initialize(*params)

@device_name_field = ecs_select[disabled: "[#{@prefix}device]", v1: '[device][name]']
@device_name_field = "#{target}#{@device_name_field}"
@device_family_field = ecs_select[disabled: "[#{@prefix}device]", v1: '[device][family]']
@device_family_field = "#{target}#{@device_family_field}"
@device_brand_field = ecs_select[disabled: "[#{@prefix}device]", v1: '[device][brand]']
@device_brand_field = "#{target}#{@device_brand_field}"
@device_model_field = ecs_select[disabled: "[#{@prefix}device]", v1: '[device][model]']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are going to be problematic as ATM ECS (1.11) only supports user_agent.device.name

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kares is there an "experimental" switch that could enable this behavior? Such that it is still compliant?

@device_model_field = "#{target}#{@device_model_field}"

@version_field = ecs_select[disabled: "[#{@prefix}version]", v1: '[version]']
@version_field = "#{target}#{@version_field}"
Expand Down Expand Up @@ -138,8 +144,14 @@ def set_fields(event, ua_source, ua_data)

ua = ua_data.userAgent
event.set(@name_field, duped_string(ua.family))
event.set(@device_name_field, duped_string(ua_data.device)) if ua_data.device

dev = ua_data.device
if dev
event.set(@device_name_field, duped_string(ua_data.device)) if dev.family
event.set(@device_family_field, duped_string(ua_data.device)) if dev.family
event.set(@device_brand_field, duped_string(ua_data.device)) if dev.brand
event.set(@device_model_field, duped_string(ua_data.device)) if dev.model
end
event.set(@major_field, duped_string(ua.major)) if ua.major
event.set(@minor_field, duped_string(ua.minor)) if ua.minor
event.set(@patch_field, duped_string(ua.patch)) if ua.patch
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/logstash/uaparser/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ public final class Client {

public final OS os;

public final String device;
public final Device device;

public Client(final UserAgent userAgent, final OS os, final String device) {
public Client(final UserAgent userAgent, final OS os, final Device device) {
this.userAgent = userAgent;
this.os = os;
this.device = device;
Expand Down
42 changes: 40 additions & 2 deletions src/main/java/org/logstash/uaparser/Device.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.logstash.uaparser;

import java.util.Map;
import java.util.Objects;

/**
* Device parsed data class
Expand All @@ -27,7 +28,44 @@
*/
final class Device {

public static String fromMap(Map<String, String> m) {
return m.get("family");
public final String family;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems that the uap-java does not have these changes:
https://github.com/ua-parser/uap-java/blob/master/src/main/java/ua_parser/Device.java

so this was written from scratch or copied from somewhere else?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kares this was written from scratch (similar to the OS class). I did not find a reference in the java class that it was from the uap-java project.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you pointed me to it - there is a PR ua-parser/uap-java#37 in the uap-java project that asks for same functionality but it hasn't seen action since early 2020. If I had seen it, I would have mentioned it.

public final String brand;
public final String model;

Device(String family, String brand, String model) {
this.family = family;
this.brand = brand;
this.model = model;
}

public static Device fromMap(Map<String, String> m) {
return new Device(m.get("family"), m.get("brand"), m.get("model"));
}

@Override
public boolean equals(Object other) {
if (other == this) return true;
if (!(other instanceof Device)) return false;
Device o = (Device) other;
return ((this.family != null && this.family.equals(o.family)) || Objects.equals(this.family, o.family)) &&
((this.brand != null && this.brand.equals(o.brand)) || Objects.equals(this.brand, o.brand)) &&
((this.model != null && this.model.equals(o.model)) || Objects.equals(this.model, o.model)) ;
}

@Override
public int hashCode() {
int h = family == null ? 0 : family.hashCode();
h += brand == null ? 0 : brand.hashCode();
h += model == null ? 0 : model.hashCode();
return h;
}

@Override
public String toString() {
return String.format("{\"family\": %s, \"brand\": %s, \"model\": %s}",
family == null ? "" : family,
brand == null ? "" : brand,
model == null ? "" : model
);
}
}
64 changes: 51 additions & 13 deletions src/main/java/org/logstash/uaparser/DeviceParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ private DeviceParser(List<DeviceParser.DevicePattern> patterns) {
this.patterns = patterns;
}

public String parse(String agentString) {
public Device parse(String agentString) {
if (agentString == null) {
return null;
}
String device = null;
Device device = null;
for (final DeviceParser.DevicePattern p : this.patterns) {
if ((device = p.match(agentString)) != null) {
break;
}
}
if (device == null) device = "Other";
if (device == null) device = new Device("Other", null, null);
return device;
}

Expand All @@ -69,7 +69,7 @@ private static DeviceParser.DevicePattern patternFromMap(Map<String, String> con
}
Pattern pattern = "i".equals(configMap.get("regex_flag")) // no other flags used (by now)
? Pattern.compile(regex, Pattern.CASE_INSENSITIVE) : Pattern.compile(regex);
return new DeviceParser.DevicePattern(pattern, configMap.get("device_replacement"));
return new DeviceParser.DevicePattern(pattern, configMap.get("device_replacement"), configMap.get("brand_replacement"), configMap.get("model_replacement"));
}

private static final class DevicePattern {
Expand All @@ -79,37 +79,75 @@ private static final class DevicePattern {
private final Matcher matcher;

private final String deviceReplacement;
private final String brandReplacement;
private final String modelReplacement;

DevicePattern(Pattern pattern, String deviceReplacement) {
DevicePattern(Pattern pattern, String deviceReplacement, String brandReplacement, String modelReplacement) {
this.matcher = pattern.matcher("");
this.deviceReplacement = deviceReplacement;
this.brandReplacement = brandReplacement;
this.modelReplacement = modelReplacement;
}

public synchronized String match(final CharSequence agentString) {
public synchronized Device match(final CharSequence agentString) {
this.matcher.reset(agentString);
if (!this.matcher.find()) {
return null;
}
String device = null;
String family = null;
String brand = null;
String model = null;
if (this.deviceReplacement != null) {
if (this.deviceReplacement.contains("$")) {
device = this.deviceReplacement;
family = this.deviceReplacement;
for (String substitution : DevicePattern
.getSubstitutions(this.deviceReplacement)) {
int i = Integer.parseInt(substitution.substring(1));
final String replacement = this.matcher.groupCount() >= i &&
this.matcher.group(i) != null
? Matcher.quoteReplacement(this.matcher.group(i)) : "";
device = device.replaceFirst('\\' + substitution, replacement);
family = family.replaceFirst('\\' + substitution, replacement);
}
device = device.trim();
family = family.trim();
} else {
device = this.deviceReplacement;
family = this.deviceReplacement;
}
} else if (this.matcher.groupCount() >= 1) {
device = this.matcher.group(1);
family = this.matcher.group(1);
}
return device;
if (this.brandReplacement != null) {
if (this.brandReplacement.contains("$")) {
brand = this.brandReplacement;
for (String substitution : DevicePattern
.getSubstitutions(this.brandReplacement)) {
int i = Integer.parseInt(substitution.substring(1));
final String replacement = this.matcher.groupCount() >= i &&
this.matcher.group(i) != null
? Matcher.quoteReplacement(this.matcher.group(i)) : "";
brand = brand.replaceFirst('\\' + substitution, replacement);
}
brand = brand.trim();
} else {
brand = this.brandReplacement;
}
}
if (this.modelReplacement != null) {
if (this.modelReplacement.contains("$")) {
model = this.modelReplacement;
for (String substitution : DevicePattern
.getSubstitutions(this.modelReplacement)) {
int i = Integer.parseInt(substitution.substring(1));
final String replacement = this.matcher.groupCount() >= i &&
this.matcher.group(i) != null
? Matcher.quoteReplacement(this.matcher.group(i)) : "";
model = model.replaceFirst('\\' + substitution, replacement);
}
model = model.trim();
} else {
model = this.modelReplacement;
}
}
return new Device(family, brand, model);
}

private static Iterable<String> getSubstitutions(String deviceReplacement) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/logstash/uaparser/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public UserAgent parseUserAgent(String agentString) {
return this.uaParser.parse(agentString);
}

public String parseDevice(String agentString) {
public Device parseDevice(String agentString) {
return this.deviceParser.parse(agentString);
}

Expand Down
42 changes: 38 additions & 4 deletions src/test/java/org/logstash/uaparser/ParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
Expand Down Expand Up @@ -88,10 +89,10 @@ public void testParseAll() {

Client expected1 = new Client(new UserAgent("Firefox", "3", "5", "5"),
new OS("Mac OS X", "10", "4", null, null),
"Mac");
new Device("Mac", "Apple", "Mac"));
Client expected2 = new Client(new UserAgent("Mobile Safari", "5", "1", null),
new OS("iOS", "5", "1", "1", null),
"iPhone");
new Device("iPhone", "Apple", "iPhone"));

MatcherAssert.assertThat(parser.parse(agentString1), is(expected1));
MatcherAssert.assertThat(parser.parse(agentString2), is(expected2));
Expand All @@ -118,6 +119,29 @@ public void testConcurrentParse() throws Exception {
}
}

@Test
public void testCustomUAWithModel() throws Exception {
String testConfig = "user_agent_parsers:\n"
+ " - regex: 'ABC([\\\\0-9]+)'\n"
+ " family_replacement: 'ABC ($1)'\n"
+ "os_parsers:\n"
+ " - regex: 'CatOS OH-HAI=/\\^\\.\\^\\\\='\n"
+ " os_replacement: 'CatOS 9000'\n"
+ "device_parsers:\n"
+ " - regex: '(iPhone|iPad|iPod)(\\d+,\\d+)'\n"
+ " device_replacement: '$1'\n"
+ " brand_replacement: 'Apple'\n"
+ " model_replacement: '$1$2'\n";

Parser testParser = parserFromStringConfig(testConfig);
Client result = testParser.parse("ABC12\\34 (iPhone10,8 CatOS OH-HAI=/^.^\\=)");
MatcherAssert.assertThat(result.userAgent.family, is("ABC (12\\34)"));
MatcherAssert.assertThat(result.os.family, is("CatOS 9000"));
MatcherAssert.assertThat(result.device.brand, is("Apple"));
MatcherAssert.assertThat(result.device.model, is("iPhone10,8"));
MatcherAssert.assertThat(result.device.family, is("iPhone"));
}

@Test
public void testReplacementQuoting() throws Exception {
String testConfig = "user_agent_parsers:\n"
Expand All @@ -134,7 +158,7 @@ public void testReplacementQuoting() throws Exception {
Client result = testParser.parse("ABC12\\34 (CashPhone-$9.0.1 CatOS OH-HAI=/^.^\\=)");
MatcherAssert.assertThat(result.userAgent.family, is("ABC (12\\34)"));
MatcherAssert.assertThat(result.os.family, is("CatOS 9000"));
MatcherAssert.assertThat(result.device, is("CashPhone $9"));
MatcherAssert.assertThat(result.device.family, is("CashPhone $9"));
}

@Test (expected=IllegalArgumentException.class)
Expand Down Expand Up @@ -191,7 +215,17 @@ void testDeviceFromYaml(String filename) {
for(Map<String, String> testCase : testCases) {

String uaString = testCase.get("user_agent_string");
MatcherAssert.assertThat(uaString, parser.parseDevice(uaString), is(Device.fromMap(testCase)));

// Test case YAML file contains one element that is not working well
Device parseDevice = parser.parseDevice(uaString);
Device testDevice = Device.fromMap(testCase);
if (Objects.equals(parseDevice.family, "HbbTV") && Objects.equals(parseDevice.brand, "Samsung") && Objects.equals(parseDevice.model, null)) {
testCase.remove("model");
testDevice = Device.fromMap(testCase);
break;
}

MatcherAssert.assertThat(uaString, parseDevice.toString(), is(testDevice.toString()));
}
}

Expand Down