diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 89b18d4..461033b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ guava = { module = "com.google.guava:guava", version.ref = "guava" } kotlin-logging = { module = "io.github.microutils:kotlin-logging-jvm", version.ref = "kotlin-logging" } logback-logstash-encoder = { module = "net.logstash.logback:logstash-logback-encoder", version.ref = "logstash" } -logback-length-splitter = { module = "com.latch:logback-length-splitting-appender", version = "0.4.0" } +logback-classic = { module = "ch.qos.logback:logback-classic" } mockk = { module = "io.mockk:mockk-jvm", version = "1.13.11" } diff --git a/platform-spring-bom/platform-spring-logging-server-config/build.gradle b/platform-spring-bom/platform-spring-logging-server-config/build.gradle index 25bb7b9..d045a0d 100644 --- a/platform-spring-bom/platform-spring-logging-server-config/build.gradle +++ b/platform-spring-bom/platform-spring-logging-server-config/build.gradle @@ -3,6 +3,6 @@ dependencies { // encoder for JSON logging runtimeOnly(libs.logback.logstash.encoder) - // log splitter - runtimeOnly(libs.logback.length.splitter) + + implementation(libs.logback.classic) } diff --git a/platform-spring-bom/platform-spring-logging-server-config/src/main/java/com/latch/LengthSplittingAppender.java b/platform-spring-bom/platform-spring-logging-server-config/src/main/java/com/latch/LengthSplittingAppender.java new file mode 100644 index 0000000..18e313b --- /dev/null +++ b/platform-spring-bom/platform-spring-logging-server-config/src/main/java/com/latch/LengthSplittingAppender.java @@ -0,0 +1,95 @@ +package com.latch; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/* + * MIT License + * + * Copyright (c) 2019 Latchable, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +public class LengthSplittingAppender extends SplittingAppenderBase { + + private int maxLength; + private String sequenceKey; + + public int getMaxLength() { + return maxLength; + } + + public void setMaxLength(int maxLength) { + this.maxLength = maxLength; + } + + public String getSequenceKey() { + return sequenceKey; + } + + public void setSequenceKey(String sequenceKey) { + this.sequenceKey = sequenceKey; + } + + @Override + public boolean shouldSplit(ILoggingEvent event) { + return event.getFormattedMessage().length() > maxLength; + } + + @Override + public List split(ILoggingEvent event) { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + List logMessages = splitString(event.getFormattedMessage(), getMaxLength()); + + List splitLogEvents = new ArrayList<>(logMessages.size()); + for (int i = 0; i < logMessages.size(); i++) { + + LoggingEvent partition = LoggingEventCloner.clone(event, loggerContext); + Map seqMDCPropertyMap = new HashMap<>(event.getMDCPropertyMap()); + seqMDCPropertyMap.put(getSequenceKey(), Integer.toString(i)); + partition.setMDCPropertyMap(seqMDCPropertyMap); + partition.setMessage(logMessages.get(i)); + + splitLogEvents.add(partition); + } + + return splitLogEvents; + } + + private List splitString(String str, int chunkSize) { + int fullChunks = str.length() / chunkSize; + int remainder = str.length() % chunkSize; + + List results = new ArrayList<>(remainder == 0 ? fullChunks : fullChunks + 1); + for (int i = 0; i < fullChunks; i++) { + results.add(str.substring(i*chunkSize, i*chunkSize + chunkSize)); + } + if (remainder != 0) { + results.add(str.substring(str.length() - remainder)); + } + return results; + } +} diff --git a/platform-spring-bom/platform-spring-logging-server-config/src/main/java/com/latch/LoggingEventCloner.java b/platform-spring-bom/platform-spring-logging-server-config/src/main/java/com/latch/LoggingEventCloner.java new file mode 100644 index 0000000..837fa58 --- /dev/null +++ b/platform-spring-bom/platform-spring-logging-server-config/src/main/java/com/latch/LoggingEventCloner.java @@ -0,0 +1,56 @@ +package com.latch; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import org.slf4j.Marker; + +import java.util.List; + +/* + * MIT License + * + * Copyright (c) 2019 Latchable, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +class LoggingEventCloner { + + static LoggingEvent clone(ILoggingEvent event, LoggerContext loggerContext) { + LoggingEvent logEventPartition = new LoggingEvent(); + + logEventPartition.setLevel(event.getLevel()); + logEventPartition.setLoggerName(event.getLoggerName()); + logEventPartition.setTimeStamp(event.getTimeStamp()); + logEventPartition.setLoggerContextRemoteView(event.getLoggerContextVO()); + logEventPartition.setLoggerContext(loggerContext); + logEventPartition.setThreadName(event.getThreadName()); + + List eventMarkers = event.getMarkerList(); + if (eventMarkers != null && !eventMarkers.isEmpty()) { + logEventPartition.getMarkerList().addAll(eventMarkers); + } + + if (event.hasCallerData()) { + logEventPartition.setCallerData(event.getCallerData()); + } + + return logEventPartition; + } +} diff --git a/platform-spring-bom/platform-spring-logging-server-config/src/main/java/com/latch/SplittingAppenderBase.java b/platform-spring-bom/platform-spring-logging-server-config/src/main/java/com/latch/SplittingAppenderBase.java new file mode 100644 index 0000000..612b9a3 --- /dev/null +++ b/platform-spring-bom/platform-spring-logging-server-config/src/main/java/com/latch/SplittingAppenderBase.java @@ -0,0 +1,80 @@ +package com.latch; + +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.UnsynchronizedAppenderBase; +import ch.qos.logback.core.spi.AppenderAttachable; +import ch.qos.logback.core.spi.AppenderAttachableImpl; + +import java.util.Iterator; +import java.util.List; + +/* + * MIT License + * + * Copyright (c) 2019 Latchable, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +public abstract class SplittingAppenderBase extends UnsynchronizedAppenderBase + implements AppenderAttachable { + + private final AppenderAttachableImpl aai = new AppenderAttachableImpl<>(); + + protected abstract List split(E event); + + protected abstract boolean shouldSplit(E eventObject); + + @Override + protected void append(E eventObject) { + if (shouldSplit(eventObject)) { + split(eventObject).forEach(aai::appendLoopOnAppenders); + } else { + aai.appendLoopOnAppenders(eventObject); + } + } + + public void addAppender(Appender newAppender) { + addInfo("Attaching appender named [" + newAppender.getName() + "] to SplittingAppender."); + aai.addAppender(newAppender); + } + + public Iterator> iteratorForAppenders() { + return aai.iteratorForAppenders(); + } + + public Appender getAppender(String name) { + return aai.getAppender(name); + } + + public boolean isAttached(Appender eAppender) { + return aai.isAttached(eAppender); + } + + public void detachAndStopAllAppenders() { + aai.detachAndStopAllAppenders(); + } + + public boolean detachAppender(Appender eAppender) { + return aai.detachAppender(eAppender); + } + + public boolean detachAppender(String name) { + return aai.detachAppender(name); + } +} diff --git a/platform-spring-bom/platform-spring-logging-server-config/src/test/java/com/latch/LengthSplittingAppenderTest.java b/platform-spring-bom/platform-spring-logging-server-config/src/test/java/com/latch/LengthSplittingAppenderTest.java new file mode 100644 index 0000000..4863aca --- /dev/null +++ b/platform-spring-bom/platform-spring-logging-server-config/src/test/java/com/latch/LengthSplittingAppenderTest.java @@ -0,0 +1,131 @@ +package com.latch; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/* + * MIT License + * + * Copyright (c) 2019 Latchable, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +public class LengthSplittingAppenderTest { + private static final int MAX_MESSAGE_LENGTH = 50; + private static final String BASE_STRING = "0123456789"; + private static final String LOREM_PATH = "logging_message.txt"; + + private final LoggerContext loggerContext; + private final LengthSplittingAppender splitter; + + public LengthSplittingAppenderTest() { + this.loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + this.splitter = new LengthSplittingAppender(); + splitter.setMaxLength(MAX_MESSAGE_LENGTH); + splitter.setSequenceKey("seq"); + Assertions.assertEquals(MAX_MESSAGE_LENGTH, splitter.getMaxLength()); + } + + @Test + public void testEmpty() { + LoggingEvent event = createLoggingEvent(""); + Assertions.assertFalse(splitter.shouldSplit(event)); + } + + @Test + public void testLessThanMax() { + LoggingEvent event = createLoggingEvent(String.join("", Collections.nCopies(1, BASE_STRING))); + Assertions.assertFalse(splitter.shouldSplit(event)); + } + + @Test + public void testEqualToMax() { + LoggingEvent event = createLoggingEvent(String.join("", Collections.nCopies(5, BASE_STRING))); + Assertions.assertEquals(MAX_MESSAGE_LENGTH, 5 * BASE_STRING.length()); + Assertions.assertFalse(splitter.shouldSplit(event)); + } + + @Test + public void testGreaterThanMaxAndMultipleOfMax() { + LoggingEvent event = createLoggingEvent(String.join("", Collections.nCopies(50, BASE_STRING))); + Assertions.assertTrue(splitter.shouldSplit(event)); + + List splitEvents = splitter.split(event); + + Assertions.assertEquals( + event.getFormattedMessage().length() / MAX_MESSAGE_LENGTH, + splitEvents.size()); + } + + @Test + public void testGreaterThanMaxAndNotMultipleOfMax() { + LoggingEvent event = createLoggingEvent(String.join("", Collections.nCopies(51, BASE_STRING))); + Assertions.assertTrue(splitter.shouldSplit(event)); + + List splitEvents = splitter.split(event); + + Assertions.assertEquals( + event.getFormattedMessage().length() / MAX_MESSAGE_LENGTH + 1, + splitEvents.size()); + } + + @Test + public void testSplitIntegrity() { + String loremIpsum = readTextFromResource(LOREM_PATH); + LoggingEvent event = createLoggingEvent(loremIpsum); + + List splitEvents = splitter.split(event); + + Assertions.assertEquals(event.getFormattedMessage(), recreateMessage(splitEvents)); + } + + private LoggingEvent createLoggingEvent(String message) { + LoggingEvent event = new LoggingEvent(); + event.setMessage(message); + event.setLoggerContext(loggerContext); + return event; + } + + private String recreateMessage(List splitEvents) { + StringBuilder sb = new StringBuilder(); + + for (ILoggingEvent splitEvent : splitEvents) { + sb.append(splitEvent.getFormattedMessage()); + } + + return sb.toString(); + } + + private String readTextFromResource(String fileName) { + InputStream is = getClass().getClassLoader().getResourceAsStream(fileName); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + return reader.lines().collect(Collectors.joining("")); + } +} diff --git a/platform-spring-bom/platform-spring-logging-server-config/src/test/java/io/cloudflight/platform/spring/logging/LoggingJsonSplitterTest.java b/platform-spring-bom/platform-spring-logging-server-config/src/test/java/io/cloudflight/platform/spring/logging/LoggingJsonSplitterTest.java new file mode 100644 index 0000000..35b59d7 --- /dev/null +++ b/platform-spring-bom/platform-spring-logging-server-config/src/test/java/io/cloudflight/platform/spring/logging/LoggingJsonSplitterTest.java @@ -0,0 +1,55 @@ +package io.cloudflight.platform.spring.logging; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.util.ContextInitializer; +import ch.qos.logback.core.joran.spi.JoranException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.LoggerFactory; +import org.slf4j.helpers.BasicMarkerFactory; + +public class LoggingJsonSplitterTest { + private static final Logger LOG = LoggerFactory.getLogger(LoggingJsonSplitterTest.class); + private final String originalLogbackFile = System.getProperty("logback.configurationFile"); + private static final Marker TEST_MARKER = new BasicMarkerFactory().getMarker("TEST_MARKER"); + + @BeforeEach + void setUp() { + System.setProperty("logback.configurationFile", "src/test/resources/logback-test-json-splitter.xml"); + configureLogback(); + } + + @AfterEach + void tearDown() { + if (originalLogbackFile != null) { + System.setProperty("logback.configurationFile", originalLogbackFile); + } else { + System.clearProperty("logback.configurationFile"); + } + configureLogback(); + } + + private void configureLogback() { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.reset(); + try { + new ContextInitializer(loggerContext).autoConfig(); + } catch (JoranException e) { + throw new RuntimeException(e); + } + } + + @Test + void logWithCloudbackConfig() { + LOG.info("Hello World"); + } + + @Test + void logLongMessageWithJsonSplitterFails() { + Assertions.assertDoesNotThrow(() -> LOG.info(TEST_MARKER, "This is a long message. ".repeat(1000))); + } +} diff --git a/platform-spring-bom/platform-spring-logging-server-config/src/test/resources/logback-test-json-splitter.xml b/platform-spring-bom/platform-spring-logging-server-config/src/test/resources/logback-test-json-splitter.xml new file mode 100644 index 0000000..7e26185 --- /dev/null +++ b/platform-spring-bom/platform-spring-logging-server-config/src/test/resources/logback-test-json-splitter.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/platform-spring-bom/platform-spring-logging-server-config/src/test/resources/logging_message.txt b/platform-spring-bom/platform-spring-logging-server-config/src/test/resources/logging_message.txt new file mode 100644 index 0000000..6b416d0 --- /dev/null +++ b/platform-spring-bom/platform-spring-logging-server-config/src/test/resources/logging_message.txt @@ -0,0 +1,19 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Euismod lacinia at quis risus sed vulputate odio ut. Facilisis mauris sit amet massa. Ipsum suspendisse ultrices gravida dictum fusce ut. A cras semper auctor neque vitae tempus quam pellentesque. Mauris in aliquam sem fringilla. Egestas sed sed risus pretium quam. Fermentum dui faucibus in ornare. Nunc pulvinar sapien et ligula ullamcorper malesuada. Volutpat odio facilisis mauris sit. Ut porttitor leo a diam sollicitudin tempor. Amet risus nullam eget felis eget nunc lobortis. Eget mauris pharetra et ultrices neque ornare. Suspendisse interdum consectetur libero id faucibus nisl. + +Fermentum posuere urna nec tincidunt praesent semper. Lacus sed viverra tellus in hac habitasse platea. Eros in cursus turpis massa tincidunt dui. Non odio euismod lacinia at. In ante metus dictum at tempor commodo ullamcorper. Convallis tellus id interdum velit laoreet id donec ultrices tincidunt. Risus commodo viverra maecenas accumsan lacus. Sagittis vitae et leo duis ut diam quam nulla. Mauris ultrices eros in cursus. Neque vitae tempus quam pellentesque nec nam aliquam. In hac habitasse platea dictumst quisque sagittis purus sit. Senectus et netus et malesuada fames ac turpis egestas. + +Quis enim lobortis scelerisque fermentum dui faucibus in ornare quam. Lacus viverra vitae congue eu consequat ac. Velit dignissim sodales ut eu sem integer vitae justo. In massa tempor nec feugiat nisl. Vivamus at augue eget arcu dictum varius duis at consectetur. Placerat orci nulla pellentesque dignissim enim sit amet. Nunc aliquet bibendum enim facilisis. Tellus cras adipiscing enim eu turpis egestas pretium aenean. Dictum varius duis at consectetur lorem donec massa sapien. Massa id neque aliquam vestibulum morbi. Sit amet nisl purus in mollis nunc sed id semper. Molestie a iaculis at erat. Ac turpis egestas maecenas pharetra. Eleifend quam adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus. Cursus turpis massa tincidunt dui ut. Bibendum est ultricies integer quis auctor elit sed. Sit amet aliquam id diam maecenas ultricies mi. Eu sem integer vitae justo. Lorem ipsum dolor sit amet consectetur adipiscing. Eu nisl nunc mi ipsum faucibus vitae aliquet. + +Fusce id velit ut tortor pretium viverra. Nibh praesent tristique magna sit amet. Cras pulvinar mattis nunc sed. Amet consectetur adipiscing elit duis tristique sollicitudin nibh. Enim tortor at auctor urna nunc id cursus. Sagittis id consectetur purus ut faucibus pulvinar. Ipsum dolor sit amet consectetur adipiscing elit. At elementum eu facilisis sed. Fusce id velit ut tortor pretium viverra suspendisse. Mauris nunc congue nisi vitae suscipit tellus mauris a diam. Eget mauris pharetra et ultrices neque ornare aenean euismod elementum. Dictum non consectetur a erat nam. Cras sed felis eget velit aliquet. Aliquam nulla facilisi cras fermentum. Suspendisse sed nisi lacus sed viverra tellus in hac. + +Cum sociis natoque penatibus et magnis dis. Rutrum tellus pellentesque eu tincidunt tortor aliquam nulla facilisi. Quam vulputate dignissim suspendisse in est ante in nibh mauris. Ut enim blandit volutpat maecenas volutpat blandit aliquam. Ut tristique et egestas quis ipsum suspendisse ultrices gravida dictum. Enim sit amet venenatis urna cursus. Suscipit tellus mauris a diam maecenas sed enim ut sem. Aliquam sem et tortor consequat. Pellentesque sit amet porttitor eget dolor morbi non. Leo vel fringilla est ullamcorper eget nulla facilisi etiam. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Cursus in hac habitasse platea dictumst quisque. Consectetur adipiscing elit duis tristique sollicitudin nibh sit amet. Felis bibendum ut tristique et egestas quis. Vehicula ipsum a arcu cursus vitae congue. Proin fermentum leo vel orci porta. Nulla pellentesque dignissim enim sit amet. Ornare suspendisse sed nisi lacus sed. Hendrerit gravida rutrum quisque non tellus orci ac auctor. Volutpat blandit aliquam etiam erat velit. Ipsum dolor sit amet consectetur adipiscing. Porttitor massa id neque aliquam vestibulum morbi. Eleifend quam adipiscing vitae proin sagittis nisl rhoncus. Diam quis enim lobortis scelerisque fermentum dui faucibus. Ac tortor vitae purus faucibus ornare. Euismod in pellentesque massa placerat duis ultricies lacus sed. Vulputate dignissim suspendisse in est ante in nibh. Condimentum lacinia quis vel eros donec. Nibh tortor id aliquet lectus proin nibh nisl. + +Eget lorem dolor sed viverra ipsum. Laoreet non curabitur gravida arcu. Enim blandit volutpat maecenas volutpat blandit aliquam etiam erat velit. Morbi leo urna molestie at elementum. Ornare suspendisse sed nisi lacus. Viverra tellus in hac habitasse platea dictumst. At auctor urna nunc id cursus metus aliquam eleifend. Ullamcorper velit sed ullamcorper morbi tincidunt. Vestibulum morbi blandit cursus risus at. Mi quis hendrerit dolor magna. Enim sed faucibus turpis in eu. + +Quam id leo in vitae turpis massa sed elementum. Eleifend mi in nulla posuere sollicitudin. Mi sit amet mauris commodo quis imperdiet. Diam volutpat commodo sed egestas. Mi ipsum faucibus vitae aliquet nec ullamcorper. Tortor aliquam nulla facilisi cras fermentum. Purus sit amet luctus venenatis. Ultrices neque ornare aenean euismod elementum nisi. At tellus at urna condimentum. Aliquet enim tortor at auctor. Condimentum id venenatis a condimentum vitae. Aliquet risus feugiat in ante metus dictum. Eros in cursus turpis massa tincidunt dui ut ornare lectus. Id venenatis a condimentum vitae sapien. Aliquam vestibulum morbi blandit cursus risus at ultrices. Pellentesque pulvinar pellentesque habitant morbi tristique senectus. Tincidunt ornare massa eget egestas purus viverra. Et ligula ullamcorper malesuada proin. Pharetra convallis posuere morbi leo urna. Magnis dis parturient montes nascetur ridiculus. + +Luctus accumsan tortor posuere ac. Id eu nisl nunc mi ipsum faucibus vitae aliquet nec. Dolor sit amet consectetur adipiscing elit pellentesque. Lacus sed turpis tincidunt id aliquet risus feugiat. Risus ultricies tristique nulla aliquet enim. Ante in nibh mauris cursus mattis. Eget nunc scelerisque viverra mauris in aliquam sem. Sed viverra tellus in hac. Ultricies leo integer malesuada nunc vel risus. Ligula ullamcorper malesuada proin libero nunc consequat interdum. Pulvinar sapien et ligula ullamcorper. + +Mattis pellentesque id nibh tortor id aliquet lectus proin nibh. Massa vitae tortor condimentum lacinia. Leo vel fringilla est ullamcorper eget nulla. Sed elementum tempus egestas sed sed risus pretium quam vulputate. Augue interdum velit euismod in. Elit pellentesque habitant morbi tristique senectus. Integer eget aliquet nibh praesent. Feugiat in fermentum posuere urna nec tincidunt praesent semper feugiat. Volutpat diam ut venenatis tellus in metus vulputate. Sapien faucibus et molestie ac feugiat sed lectus. Lectus proin nibh nisl condimentum id venenatis a. \ No newline at end of file