diff --git a/src/main/java/com/cloudbees/syslog/SDElement.java b/src/main/java/com/cloudbees/syslog/SDElement.java new file mode 100644 index 0000000..f58f0e2 --- /dev/null +++ b/src/main/java/com/cloudbees/syslog/SDElement.java @@ -0,0 +1,158 @@ +/* + * Copyright 2010-2014, CloudBees Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cloudbees.syslog; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * A SD-ELEMENT + * + * @author Brett Bergquist + */ +public class SDElement implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Reserved SD-IDs as documented in RFC-5424 + */ + public static final String[] RESERVED_SDID = new String[]{ + "timeQuality", + "tzKnown", + "isSynced", + "syncAccuracy" + }; + + public SDElement(String sdID) { + validateSDID(sdID); + this.sdID = sdID; + } + + public SDElement(String sdID, SDParam... sdParams) { + validateSDID(sdID); + this.sdID = sdID; + this.sdParams.addAll(Arrays.asList(sdParams)); + } + + private String sdID; + + /** + * Get the value of sdID + * + * @return the value of sdID + */ + public String getSdID() { + return sdID; + } + + private List sdParams = new ArrayList(); + + /** + * Get the value of sdParams + * + * @return the value of sdParams + */ + public List getSdParams() { + return sdParams; + } + + /** + * Set the value of sdParams + * + * @param sdParams new value of sdParams + */ + public void setSdParams(List sdParams) { + if (null == sdParams) { + throw new IllegalArgumentException("sdParams list cannot be null"); + } + this.sdParams.addAll(sdParams); + } + + /** + * Adds a SDParam + * @param paramName the PARAM-NAME + * @param paramValue the PARAM-VALUE + * @return + */ + public SDElement addSDParam(String paramName, String paramValue) { + return addSDParam(new SDParam(paramName, paramValue)); + } + + public SDElement addSDParam(SDParam sdParam) { + this.sdParams.add(sdParam); + return this; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + Objects.hashCode(this.sdID); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SDElement other = (SDElement) obj; + return Objects.equals(this.sdID, other.sdID); + } + + private void validateSDID(String sdName) { + if (null == sdName) { + throw new IllegalArgumentException("SD-ID cannot be null"); + } + if (sdName.length() > 32) { + throw new IllegalArgumentException("SD-ID must be less than 32 characters: " + sdName); + } + if (sdName.contains("=")) { + throw new IllegalArgumentException("SD-ID cannot contain '='"); + } + if (sdName.contains(" ")) { + throw new IllegalArgumentException("SD-ID cannot contain ' '"); + } + if (sdName.contains("]")) { + throw new IllegalArgumentException("SD-ID cannot contain ']'"); + } + if (sdName.contains("\"")) { + throw new IllegalArgumentException("SD-ID cannot contain '\"'"); + } + if (! sdName.contains("@")) { + boolean found = false; + for (String rn : RESERVED_SDID) { + if (rn.equals(sdName)) { + found = true; + break; + } + } + if (! found) { + throw new IllegalArgumentException("SD-ID is not known registered SDID: " + sdName); + } + } + } + +} diff --git a/src/main/java/com/cloudbees/syslog/SDParam.java b/src/main/java/com/cloudbees/syslog/SDParam.java new file mode 100644 index 0000000..de0527f --- /dev/null +++ b/src/main/java/com/cloudbees/syslog/SDParam.java @@ -0,0 +1,131 @@ +/* + * Copyright 2010-2014, CloudBees Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cloudbees.syslog; + +import java.io.Serializable; +import java.util.Objects; + +/** + * + * @author Brett Bergquist + */ +public class SDParam implements Serializable { + + private static final long serialVersionUID = 1L; + + public SDParam(String paramName, String paramValue) { + validateParamName(paramName); + this.paramName = paramName; + this.paramValue = paramValue; + } + + private String paramName; + + /** + * Get the value of paramName + * + * @return the value of paramName + */ + public String getParamName() { + return paramName; + } + + /** + * Set the value of paramName + * + * @param paramName new value of paramName + */ + public void setParamName(String paramName) { + validateParamName(paramName); + this.paramName = paramName; + } + + private String paramValue; + + /** + * Get the value of paramValue + * + * @return the value of paramValue + */ + public String getParamValue() { + return paramValue; + } + + /** + * Set the value of paramValue + * + * @param paramValue new value of paramValue + */ + public void setParamValue(String paramValue) { + this.paramValue = paramValue; + } + + private void validateParamName(String sdName) { + if (null == sdName) { + throw new IllegalArgumentException("PARAM-NAME cannot be null"); + } + if (sdName.length() > 32) { + throw new IllegalArgumentException("PARAM-NAME must be less than 32 characters: " + sdName); + } + if (sdName.contains("=")) { + throw new IllegalArgumentException("PARAM-NAME cannot contain '='"); + } + if (sdName.contains(" ")) { + throw new IllegalArgumentException("PARAM-NAME cannot contain ' '"); + } + if (sdName.contains("]")) { + throw new IllegalArgumentException("PARAM-NAME cannot contain ']'"); + } + if (sdName.contains("\"")) { + throw new IllegalArgumentException("PARAM-NAME cannot contain '\"'"); + } + } + + @Override + public int hashCode() { + int hash = 7; + hash = 59 * hash + Objects.hashCode(this.paramName); + hash = 59 * hash + Objects.hashCode(this.paramValue); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SDParam other = (SDParam) obj; + if (!Objects.equals(this.paramName, other.paramName)) { + return false; + } + if (!Objects.equals(this.paramValue, other.paramValue)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "SDParam{" + "paramName=" + paramName + ", paramValue=" + paramValue + '}'; + } + +} diff --git a/src/main/java/com/cloudbees/syslog/SyslogMessage.java b/src/main/java/com/cloudbees/syslog/SyslogMessage.java index 87e065c..fb499e4 100644 --- a/src/main/java/com/cloudbees/syslog/SyslogMessage.java +++ b/src/main/java/com/cloudbees/syslog/SyslogMessage.java @@ -27,7 +27,9 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Date; +import java.util.HashSet; import java.util.Locale; +import java.util.Set; import java.util.TimeZone; import java.util.concurrent.TimeUnit; @@ -91,6 +93,7 @@ protected String newObject() { private String appName; private String procId; private String msgId; + private Set sdElements; /** * Use a {@link java.io.CharArrayWriter} instead of a {@link String} or a {@code char[]} because middlewares like * Apache Tomcat use {@code CharArrayWriter} and it's convenient for pooling objects. @@ -213,6 +216,26 @@ public SyslogMessage withMsg(final String msg) { } }); } + + public Set getSDElements() { + Set ssde = sdElements; + if (ssde == null) { + ssde = new HashSet(0); + } + return ssde; + } + + public void setSDElements(Set ssde) { + this.sdElements = ssde; + } + + public SyslogMessage withSDElement(SDElement sde) { + if (sdElements == null) { + sdElements = new HashSet(); + } + sdElements.add(sde); + return this; + } /** * Generates a Syslog message complying to the RFC-5424 format @@ -287,7 +310,7 @@ public void toRfc5424SyslogMessage(Writer out) throws IOException { out.write(SP); writeNillableValue(msgId, out);// Message ID out.write(SP); - out.write(NILVALUE); // structured data + writeStructuredDataOrNillableValue(sdElements, out); if (msg != null) { out.write(SP); msg.writeTo(out); @@ -339,4 +362,53 @@ protected void writeNillableValue(@Nullable String value, @Nonnull Writer out) t out.write(value); } } + + protected void writeStructuredDataOrNillableValue(@Nullable Set ssde, @Nonnull Writer out) throws IOException { + if (ssde == null || ssde.isEmpty()) { + out.write(NILVALUE); + } else { + for (SDElement sde : ssde) { + writeSDElement(sde, out); + } + } + } + + protected void writeSDElement(@Nonnull SDElement sde, @Nonnull Writer out) throws IOException { + out.write("["); + out.write(sde.getSdID()); + for (SDParam sdp : sde.getSdParams()) { + writeSDParam(sdp, out); + } + out.write("]"); + } + + protected void writeSDParam(@Nonnull SDParam sdp, @Nonnull Writer out) throws IOException { + out.write(SP); + out.write(sdp.getParamName()); + out.write('='); + out.write('"'); + out.write(getEscapedParamValue(sdp.getParamValue())); + out.write('"'); + } + + protected String getEscapedParamValue(String paramValue) { + StringBuilder sb = new StringBuilder(paramValue.length()); + + for (int i = 0; i < paramValue.length(); i++) { + char c = paramValue.charAt(i); + switch (c) { + // Falls through + case '"': + case '\\': + case ']': + sb.append('\\'); + break; + default: + break; + } + sb.append(c); + } + + return sb.toString(); + } } diff --git a/src/test/java/com/cloudbees/syslog/SyslogMessageTest.java b/src/test/java/com/cloudbees/syslog/SyslogMessageTest.java index fb53cc6..b940323 100644 --- a/src/test/java/com/cloudbees/syslog/SyslogMessageTest.java +++ b/src/test/java/com/cloudbees/syslog/SyslogMessageTest.java @@ -56,6 +56,39 @@ public void testRfc5424Format() throws Exception { } + @Test + public void testRfc5424FormatWithStructuredData() throws Exception { + Calendar cal = Calendar.getInstance(); + cal.setTimeZone(TimeZone.getTimeZone("GMT")); + cal.set(2013, Calendar.DECEMBER, 5, 10, 30, 5); + cal.set(Calendar.MILLISECOND, 0); + + System.out.println(SyslogMessage.rfc3339DateFormat.format(cal.getTime())); + System.out.println(cal.getTimeInMillis()); + + + SyslogMessage message = new SyslogMessage() + .withTimestamp(cal.getTimeInMillis()) + .withAppName("my_app") + .withHostname("myserver.example.com") + .withFacility(Facility.USER) + .withSeverity(Severity.INFORMATIONAL) + .withTimestamp(cal.getTimeInMillis()) + .withMsg("a syslog message") + .withSDElement(new SDElement("exampleSDID@32473", new SDParam("iut", "3"), new SDParam("eventSource", "Application"), new SDParam("eventID", "1011"))); + + String actual = message.toRfc5424SyslogMessage(); + String expected = "<14>1 2013-12-05T10:30:05.000Z myserver.example.com my_app - - [exampleSDID@32473 iut=\"3\" eventSource=\"Application\" eventID=\"1011\"] a syslog message"; + + assertThat(actual, is(expected)); + + message.withSDElement(new SDElement("examplePriority@32473", new SDParam("class", "high"))); + actual = message.toRfc5424SyslogMessage(); + expected = "<14>1 2013-12-05T10:30:05.000Z myserver.example.com my_app - - [exampleSDID@32473 iut=\"3\" eventSource=\"Application\" eventID=\"1011\"][examplePriority@32473 class=\"high\"] a syslog message"; + + assertThat(actual, is(expected)); + } + @Test public void testRfc3164Format() throws Exception {