diff --git a/core/src/main/java/com/dtolabs/rundeck/core/utils/LogBuffer.java b/core/src/main/java/com/dtolabs/rundeck/core/utils/LogBuffer.java new file mode 100644 index 00000000000..11f213545b3 --- /dev/null +++ b/core/src/main/java/com/dtolabs/rundeck/core/utils/LogBuffer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 Rundeck, Inc. (http://rundeck.com) + * + * 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.dtolabs.rundeck.core.utils; + +/** + * Buffer for capturing data into another data type + * + * @param data type + */ +public interface LogBuffer { + /** + * @return true if empty + */ + boolean isEmpty(); + + /** + * reset buffer + */ + void reset(); + + /** + * write a byte + * + * @param b data + */ + void write(byte b); + + /** + * Clear the buffer + */ + void clear(); + + /** + * @return result datatype + */ + D get(); +} diff --git a/core/src/main/java/com/dtolabs/rundeck/core/utils/LogBufferManager.java b/core/src/main/java/com/dtolabs/rundeck/core/utils/LogBufferManager.java new file mode 100644 index 00000000000..41f8b7dad8e --- /dev/null +++ b/core/src/main/java/com/dtolabs/rundeck/core/utils/LogBufferManager.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Rundeck, Inc. (http://rundeck.com) + * + * 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.dtolabs.rundeck.core.utils; + +import java.nio.charset.Charset; +import java.util.function.Consumer; + +/** + * Manager for log buffers for a data type + * + * @param datatype + * @param log buffer type + */ +public interface LogBufferManager> { + /** + * Create a new log buffer with the charset + * + * @param charset charset + */ + T create(Charset charset); + + /** + * Flush all buffers with the consumer + * + * @param writer consumer of log events + */ + void flush(Consumer writer); +} diff --git a/core/src/main/java/com/dtolabs/rundeck/core/utils/StringLogBuffer.java b/core/src/main/java/com/dtolabs/rundeck/core/utils/StringLogBuffer.java new file mode 100644 index 00000000000..f6d5e04f704 --- /dev/null +++ b/core/src/main/java/com/dtolabs/rundeck/core/utils/StringLogBuffer.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 Rundeck, Inc. (http://rundeck.com) + * + * 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.dtolabs.rundeck.core.utils; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.Charset; + +/** + * Implements basic buffer of data into a String + */ +public class StringLogBuffer + implements LogBuffer +{ + private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private final Charset charset; + + public StringLogBuffer(final Charset charset) { + this.charset = charset; + } + + @Override + public boolean isEmpty() { + return baos.size() == 0; + } + + @Override + public void reset() { + baos = new ByteArrayOutputStream(); + } + + @Override + public void write(final byte b) { + baos.write(b); + } + + @Override + public void clear() { + reset(); + } + + /** + * @return contents as string + */ + public String get() { + return charset != null ? new String(baos.toByteArray(), charset) : new String(baos.toByteArray()); + } + +} diff --git a/core/src/main/java/com/dtolabs/rundeck/core/utils/StringLogManager.java b/core/src/main/java/com/dtolabs/rundeck/core/utils/StringLogManager.java new file mode 100644 index 00000000000..5e6ef729509 --- /dev/null +++ b/core/src/main/java/com/dtolabs/rundeck/core/utils/StringLogManager.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 Rundeck, Inc. (http://rundeck.com) + * + * 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.dtolabs.rundeck.core.utils; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Managers string log buffers + */ +public class StringLogManager + implements LogBufferManager + +{ + private List buffers = new ArrayList<>(); + private Charset charset; + + public StringLogManager(final Charset charset) { + this.charset = charset; + } + + @Override + public StringLogBuffer create(final Charset charset) { + StringLogBuffer buffer = new StringLogBuffer(charset != null ? charset : this.charset); + buffers.add(buffer); + return buffer; + } + + @Override + public void flush(final Consumer writer) { + for (StringLogBuffer buffer : buffers) { + writer.accept(buffer.get()); + buffer.clear(); + } + buffers.clear(); + } + + public static StringLogManager factory(Charset charset) { + return new StringLogManager(charset); + } +} diff --git a/core/src/main/java/com/dtolabs/rundeck/core/utils/ThreadBoundLogOutputStream.java b/core/src/main/java/com/dtolabs/rundeck/core/utils/ThreadBoundLogOutputStream.java new file mode 100644 index 00000000000..fc241888bb6 --- /dev/null +++ b/core/src/main/java/com/dtolabs/rundeck/core/utils/ThreadBoundLogOutputStream.java @@ -0,0 +1,171 @@ +/* + * Copyright 2016 SimplifyOps, Inc. (http://simplifyops.com) + * + * 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.dtolabs.rundeck.core.utils; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Thread local buffered log output + * 解决rundeck中文log乱码的问题,此文件在rundeck源文件路径:./rundeckapp/src/groovy/com/dtolabs/rundeck/app/internal/logging/ThreadBoundLogOutputStream.groovy + * yeml#ucweb.com + */ +public class ThreadBoundLogOutputStream> + extends OutputStream +{ + private Consumer logger; + private ThreadLocal> buffer = new ThreadLocal<>(); + private InheritableThreadLocal> manager = new InheritableThreadLocal<>(); + private InheritableThreadLocal charset = new InheritableThreadLocal<>(); + private Function> factory; + + @Data + @RequiredArgsConstructor + private static class Holder { + final X buffer; + boolean crchar; + + public void clear() { + crchar = false; + buffer.clear(); + } + + public void reset() { + crchar = false; + buffer.reset(); + } + } + /** + * Create a new thread local buffered stream + * @param logger logger for events + */ + public ThreadBoundLogOutputStream( + Consumer logger, + Charset charset, + Function> factory + ) + { + this.logger = logger; + this.charset.set(charset); + this.factory = factory; + } + /** + * Set the charset to use + * @param charset new charset + * @return previous charset + */ + public Charset setCharset(Charset charset) { + Charset prev = this.charset.get(); + this.charset.set(charset); + return prev; + } + + /** + * Install a new inherited thread local buffer manager and return it + * @return manager + */ + public LogBufferManager installManager() { + LogBufferManager manager = factory.apply(charset.get()); + this.manager.set(manager); + return manager; + } + + /** + * If no manager is set, install one, otherwise return the existing one + * @return + */ + private LogBufferManager getOrCreateManager() { + if (null == manager.get()) { + installManager(); + } + return manager.get(); + } + + /** + * Write output + * @param b + */ + public void write(final int b) { + Holder log = getOrReset(); + if (b == '\n') { + flushEventBuffer(); + } else if (b == '\r') { + log.setCrchar(true); + } else { + if (log.isCrchar()) { + flushEventBuffer(); + resetEventBuffer(); + } + log.getBuffer().write((byte) b); + } + + } + + /** + * get the thread's event buffer, reset it if it is empty + * @return + */ + private Holder getOrReset() { + if (buffer.get() == null || buffer.get().getBuffer().isEmpty()) { + resetEventBuffer(); + } + return buffer.get(); + } + + /** + * reset existing or create a new buffer with the current context + */ + private void resetEventBuffer() { + if (buffer.get() == null) { + buffer.set(new Holder<>(getOrCreateManager().create(charset.get()))); + } else { + buffer.get().reset(); + } + } + + /** + * emit a log event for the current contents of the buffer + */ + private void flushEventBuffer() { + Holder holder = buffer.get(); + logger.accept(holder.getBuffer().get()); + holder.clear(); + } + + /** + * Flush all event buffers managed by the current manager + */ + public void flushBuffers() { + getOrCreateManager().flush(logger); + } + + public void flush() { + } + + @Override + public void close() throws IOException { + flushBuffers(); + + super.close(); + } +} diff --git a/core/src/test/groovy/com/dtolabs/rundeck/core/utils/ThreadBoundLogOutputStreamSpec.groovy b/core/src/test/groovy/com/dtolabs/rundeck/core/utils/ThreadBoundLogOutputStreamSpec.groovy new file mode 100644 index 00000000000..b7ca35525d7 --- /dev/null +++ b/core/src/test/groovy/com/dtolabs/rundeck/core/utils/ThreadBoundLogOutputStreamSpec.groovy @@ -0,0 +1,199 @@ +/* + * Copyright 2016 SimplifyOps, Inc. (http://simplifyops.com) + * + * 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.dtolabs.rundeck.core.utils + + +import spock.lang.Specification + +import java.nio.charset.Charset +import java.util.function.Consumer + +/** + * Created by greg on 2/18/16. + */ +class ThreadBoundLogOutputStreamSpec extends Specification { + static class TestBuffer extends StringLogBuffer { + TestBuffer(final Charset charset) { + super(charset) + } + } + + static class TestManager implements LogBufferManager { + List buffers = [] + + @Override + TestBuffer create(final Charset charset) { + buffers << new TestBuffer() + return buffers[-1] + } + + @Override + void flush(final Consumer writer) { + buffers.each { writer.accept(it.get()) } + buffers = [] + } + } + def "write without newline"() { + given: + List buffs = [] + ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream( + { TestBuffer buff -> buffs.push buff }, + Charset.defaultCharset(), + { new TestManager() } + ) + + + when: + stream.write('abc no newline'.bytes) + + then: + 0 == buffs.size() + + } + + def "write multiple without newline"() { + given: + List buffs = [] + ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream( + { TestBuffer buff -> buffs.push buff }, + Charset.defaultCharset(), + { new TestManager() } + ) + + + when: + stream.write('abc no newline'.bytes) + stream.write(' still not'.bytes) + stream.write(' more not'.bytes) + + then: + 0 == buffs.size() + + } + + def "write multiple with flush without newline"() { + given: + List buffs = [] + ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream( + { TestBuffer buff -> buffs.push buff }, + Charset.defaultCharset(), + { new TestManager() } + ) + + + when: + stream.write('abc no newline'.bytes) + stream.flush() + stream.write(' still not'.bytes) + stream.flush() + stream.write(' more not'.bytes) + stream.flush() + + then: + 0 == buffs.size() + + } + + def "write with newline"() { + given: + List buffs = [] + ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream( + { String buff -> buffs.push buff }, + Charset.defaultCharset(), + { new TestManager() } + ) + + when: + stream.write('abc yes newline\n'.bytes) + + then: + 1 == buffs.size() + } + + def "write multi then newline"() { + given: + List buffs = [] + ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream( + { String buff -> buffs.push buff }, + Charset.defaultCharset(), + { new TestManager() } + ) + + when: + stream.write('no newline'.bytes) + stream.write(' still not'.bytes) + stream.write(' more not'.bytes) + stream.write(' then\n'.bytes) + + then: + 1 == buffs.size() + } + + def "write without newline then close"() { + given: + List buffs = [] + ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream( + { String buff -> buffs.push buff }, + Charset.defaultCharset(), + { new TestManager() } + ) + + when: + stream.write('no newline'.bytes) + stream.close() + + then: + 1 == buffs.size() + } + + def "write multi without newline then close"() { + given: + List buffs = [] + ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream( + { String buff -> buffs.push buff }, + Charset.defaultCharset(), + { new TestManager() } + ) + + when: + stream.write('no newline'.bytes) + stream.write(' still not'.bytes) + stream.write(' more not'.bytes) + stream.close() + + then: + 1 == buffs.size() + } + + def "write multi without newline with cr"() { + given: + List buffs = [] + ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream( + { String buff -> buffs.push buff }, + Charset.defaultCharset(), + { new TestManager() } + ) + + when: + stream.write('no newline'.bytes) + stream.write(' with cr\rmonkey'.bytes) + + then: + 1 == buffs.size() + } + +} diff --git a/rundeckapp/grails-app/services/rundeck/services/LoggingService.groovy b/rundeckapp/grails-app/services/rundeck/services/LoggingService.groovy index 49669d0453c..acb424ce4c2 100644 --- a/rundeckapp/grails-app/services/rundeck/services/LoggingService.groovy +++ b/rundeckapp/grails-app/services/rundeck/services/LoggingService.groovy @@ -16,12 +16,16 @@ package rundeck.services +import com.dtolabs.rundeck.app.internal.logging.LogEventBuffer +import com.dtolabs.rundeck.app.internal.logging.LogEventBufferManager import com.dtolabs.rundeck.app.internal.logging.LogFlusher -import com.dtolabs.rundeck.app.internal.logging.ThreadBoundLogOutputStream import com.dtolabs.rundeck.core.execution.Contextual +import com.dtolabs.rundeck.core.logging.LogEvent import com.dtolabs.rundeck.core.logging.LogLevel import com.dtolabs.rundeck.core.logging.StreamingLogWriter import com.dtolabs.rundeck.core.plugins.configuration.PropertyScope +import com.dtolabs.rundeck.core.utils.LogBuffer +import com.dtolabs.rundeck.core.utils.ThreadBoundLogOutputStream import com.dtolabs.rundeck.plugins.logging.StreamingLogReaderPlugin import com.dtolabs.rundeck.plugins.logging.StreamingLogWriterPlugin import com.dtolabs.rundeck.server.plugins.services.StreamingLogReaderPluginProviderService @@ -263,7 +267,13 @@ class LoggingService implements ExecutionFileProducer { Charset charset=null ) { - def stream = new ThreadBoundLogOutputStream(logWriter, level, listener, charset) + def stream = new ThreadBoundLogOutputStream( + logWriter.&addEvent, + charset, + { Charset charset1 -> + LogEventBufferManager.createManager(level,listener, charset1) + } + ) flusherWorkflowListener.logOut=stream return stream } diff --git a/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBuffer.groovy b/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBuffer.groovy index f5464a2086a..f8655124b32 100644 --- a/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBuffer.groovy +++ b/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBuffer.groovy @@ -16,28 +16,33 @@ package com.dtolabs.rundeck.app.internal.logging +import com.dtolabs.rundeck.core.execution.Contextual import com.dtolabs.rundeck.core.logging.LogEvent import com.dtolabs.rundeck.core.logging.LogLevel import com.dtolabs.rundeck.core.logging.LogUtil +import com.dtolabs.rundeck.core.utils.LogBuffer import java.nio.charset.Charset import java.util.concurrent.atomic.AtomicLong /** - * Buffer of log event data used by {@link ThreadBoundLogOutputStream} + * Buffer of log event data used by {@link com.dtolabs.rundeck.core.utils.ThreadBoundLogOutputStream} */ -class LogEventBuffer implements Comparable { +class LogEventBuffer implements Comparable, LogBuffer { Date time Map context - Boolean crchar + Contextual listener ByteArrayOutputStream baos Long serial final static AtomicLong counter = new AtomicLong(0) private Charset charset + private LogLevel level - LogEventBuffer(final Map context, Charset charset = null) { + LogEventBuffer(LogLevel level, final Contextual listener, Charset charset = null) { + this.level = level serial = counter.incrementAndGet() - reset(context) + this.listener = listener + reset(listener.context) this.charset = charset } @@ -49,16 +54,24 @@ class LogEventBuffer implements Comparable { this.time = null this.context = null this.baos = new ByteArrayOutputStream() - this.crchar = false } + void reset() { + reset(listener.context) + } void reset(final Map context) { this.time = new Date() this.context = context this.baos = new ByteArrayOutputStream() } - LogEvent createEvent(LogLevel level) { + @Override + void write(final byte b) { + baos.write(b) + } + + + LogEvent get() { def string = baos?(charset?new String(baos.toByteArray(), (Charset) charset):new String(baos.toByteArray())):'' return new DefaultLogEvent( loglevel: level, diff --git a/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferManager.groovy b/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferManager.groovy index 560db24ef01..453bfd3cefd 100644 --- a/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferManager.groovy +++ b/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferManager.groovy @@ -16,39 +16,39 @@ package com.dtolabs.rundeck.app.internal.logging +import com.dtolabs.rundeck.core.execution.Contextual +import com.dtolabs.rundeck.core.logging.LogEvent import com.dtolabs.rundeck.core.logging.LogLevel -import com.dtolabs.rundeck.core.logging.StreamingLogWriter +import com.dtolabs.rundeck.core.utils.LogBufferManager import java.nio.charset.Charset +import java.util.function.Consumer /** * Creates log event buffers, and retains references to them, so that they can be * flushed later even if any thread-local references are lost when a thread finishes */ -class LogEventBufferManager { +class LogEventBufferManager implements LogBufferManager { Set buffers = new TreeSet<>() Charset charset + Contextual listener + LogLevel level - LogEventBuffer create(Map context, Charset charset = null) { - def buffer = new LogEventBuffer(context, charset ?: this.charset) + LogEventBuffer create(Charset charset = null) { + def buffer = new LogEventBuffer(level,listener, charset ?: this.charset) buffers.add(buffer) return buffer } - static LogEventBufferManager createManager(Charset charset = null) { - new LogEventBufferManager(charset: charset) + static LogEventBufferManager createManager(LogLevel level, Contextual listener, Charset charset = null) { + new LogEventBufferManager(level: level, listener: listener, charset: charset) } - /** - * flush all incomplete event buffers in the appropriate order, then clear all buffers from the cache - * @param writer - * @param level - */ - void flush(StreamingLogWriter writer, LogLevel level) { - + @Override + void flush(final Consumer writer) { for (def b : buffers) { if (!b.isEmpty()) { - writer.addEvent(b.createEvent(level)) + writer.accept(b.get()) } b.clear() } diff --git a/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogFlusher.groovy b/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogFlusher.groovy index 5057af38480..84d2c5ad3ef 100644 --- a/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogFlusher.groovy +++ b/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/LogFlusher.groovy @@ -25,6 +25,7 @@ import com.dtolabs.rundeck.core.execution.workflow.StepExecutionContext import com.dtolabs.rundeck.core.execution.workflow.steps.StepExecutor import com.dtolabs.rundeck.core.execution.workflow.steps.node.NodeStepExecutionItem import com.dtolabs.rundeck.core.execution.workflow.steps.node.NodeStepResult +import com.dtolabs.rundeck.core.utils.ThreadBoundLogOutputStream /** * Manages cleanup for buffered log events, flushes the thread bound log output streams of any buffered events when a diff --git a/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/ThreadBoundLogOutputStream.groovy b/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/ThreadBoundLogOutputStream.groovy deleted file mode 100644 index d7fddd3ac12..00000000000 --- a/rundeckapp/src/main/groovy/com/dtolabs/rundeck/app/internal/logging/ThreadBoundLogOutputStream.groovy +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2016 SimplifyOps, Inc. (http://simplifyops.com) - * - * 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.dtolabs.rundeck.app.internal.logging - -import com.dtolabs.rundeck.core.execution.Contextual -import com.dtolabs.rundeck.core.logging.LogLevel -import com.dtolabs.rundeck.core.logging.StreamingLogWriter -import com.dtolabs.rundeck.core.utils.ThreadBoundOutputStream - -import java.nio.charset.Charset - -/** - * Thread local buffered log output - * 解决rundeck中文log乱码的问题,此文件在rundeck源文件路径:./rundeckapp/src/groovy/com/dtolabs/rundeck/app/internal/logging/ThreadBoundLogOutputStream.groovy - * yeml#ucweb.com - */ -class ThreadBoundLogOutputStream extends OutputStream { - StreamingLogWriter logger - LogLevel level - Contextual contextual - ThreadLocal buffer = new ThreadLocal() - InheritableThreadLocal manager = new InheritableThreadLocal() - InheritableThreadLocal charset = new InheritableThreadLocal() - - /** - * Create a new thread local buffered stream - * @param logger logger for events - * @param level loglevel - * @param contextual source of context - */ - ThreadBoundLogOutputStream(StreamingLogWriter logger, LogLevel level, Contextual contextual, Charset charset=null) { - this.logger = logger - this.level = level - this.contextual = contextual - this.charset.set(charset) - } - /** - * Set the charset to use - * @param charset new charset - * @return previous charset - */ - public Charset setCharset(Charset charset) { - Charset prev=this.charset.get() - this.charset.set(charset) - return prev - } - - /** - * Install a new inherited thread local buffer manager and return it - * @return manager - */ - public LogEventBufferManager installManager() { - def manager = LogEventBufferManager.createManager(charset.get()) - this.manager.set(manager) - return manager - } - - /** - * If no manager is set, install one, otherwise return the existing one - * @return - */ - LogEventBufferManager getOrCreateManager() { - if (null == manager.get()) { - installManager() - } - return manager.get() - } - - /** - * Write output - * @param b - */ - public void write(final int b) { - def log = getOrReset() - if (b == (char) '\n') { - flushEventBuffer(); - } else if (b == (char) '\r') { - log.crchar = true - } else { - if (log.crchar) { - flushEventBuffer() - resetEventBuffer() - } - log.baos.write((byte) b) - } - - } - - /** - * get the thread's event buffer, reset it if it is empty - * @return - */ - private LogEventBuffer getOrReset() { - if (buffer.get() == null || buffer.get().isEmpty()) { - resetEventBuffer() - } - return buffer.get() - } - - /** - * reset existing or create a new buffer with the current context - */ - private void resetEventBuffer() { - if (!buffer.get()) { - buffer.set(getOrCreateManager().create(contextual.getContext())) - } else { - buffer.get().reset(contextual.getContext()) - } - } - - /** - * emit a log event for the current contents of the buffer - */ - private void flushEventBuffer() { - def logstate = buffer.get() - logger.addEvent(logstate.createEvent(level)) - logstate.clear() - } - - /** - * Flush all event buffers managed by the current manager - */ - public void flushBuffers() { - getOrCreateManager().flush(logger, level) - } - - public void flush() { - } - - @Override - void close() throws IOException { - flushBuffers() - - super.close() - } -} diff --git a/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferManagerSpec.groovy b/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferManagerSpec.groovy index 6a0302eae96..c1fd8cb02c9 100644 --- a/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferManagerSpec.groovy +++ b/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferManagerSpec.groovy @@ -26,10 +26,10 @@ import spock.lang.Specification class LogEventBufferManagerSpec extends Specification { def "create buffer"() { given: - def manager = new LogEventBufferManager() + def manager = new LogEventBufferManager(listener: {[abc: 'xyz']}) when: - def buffer = manager.create([abc: 'xyz']) + def buffer = manager.create() then: buffer != null @@ -42,12 +42,12 @@ class LogEventBufferManagerSpec extends Specification { def "create multiple buffers"() { given: - def manager = new LogEventBufferManager() + def manager = new LogEventBufferManager(listener: {[abc: 'xyz']}) when: - def buffer = manager.create([abc: 'xyz']) - def buffer2 = manager.create([abc: 'def']) - def buffer3 = manager.create([abc: 'ghi']) + def buffer = manager.create() + def buffer2 = manager.create() + def buffer3 = manager.create() then: manager.buffers.size() == 3 @@ -55,15 +55,15 @@ class LogEventBufferManagerSpec extends Specification { def "flush buffers"() { given: - def manager = new LogEventBufferManager() - def buffer = manager.create([abc: 'xyz']) - def buffer2 = manager.create([abc: 'def']) - def buffer3 = manager.create([abc: 'ghi']) + def manager = new LogEventBufferManager(listener: {[abc: 'xyz']}) + def buffer = manager.create() + def buffer2 = manager.create() + def buffer3 = manager.create() def writer = Mock(StreamingLogWriter) when: buffer.baos.write('abc'.bytes) - manager.flush(writer, LogLevel.DEBUG) + manager.flush({writer.addEvent(it)}) then: 3 * writer.addEvent(_) manager.buffers.isEmpty() @@ -71,16 +71,16 @@ class LogEventBufferManagerSpec extends Specification { def "flush buffers after clear"() { given: - def manager = new LogEventBufferManager() - def buffer = manager.create([abc: 'xyz']) - def buffer2 = manager.create([abc: 'def']) - def buffer3 = manager.create([abc: 'ghi']) + def manager = new LogEventBufferManager(listener: {[abc: 'xyz']}) + def buffer = manager.create() + def buffer2 = manager.create() + def buffer3 = manager.create() def writer = Mock(StreamingLogWriter) when: buffer.clear() buffer2.clear() buffer3.clear() - manager.flush(writer, LogLevel.DEBUG) + manager.flush({writer.addEvent(it.get())}) then: 0 * writer.addEvent(_) manager.buffers.isEmpty() diff --git a/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferSpec.groovy b/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferSpec.groovy index 91bb2719235..085e49edb5e 100644 --- a/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferSpec.groovy +++ b/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/LogEventBufferSpec.groovy @@ -30,7 +30,7 @@ class LogEventBufferSpec extends Specification { def "clear buffer is empty"() { given: - def buff = new LogEventBuffer([:]) + def buff = new LogEventBuffer(null,{[:]}) when: buff.clear() then: @@ -38,27 +38,27 @@ class LogEventBufferSpec extends Specification { 0 == buff.baos.size() null == buff.context null == buff.time - !buff.crchar + buff.isEmpty() } def "new buffer not empty"() { given: - def buff = new LogEventBuffer([:]) + def buff = new LogEventBuffer(null,{[:]}) expect: null != buff.baos 0 == buff.baos.size() null != buff.context null != buff.time - !buff.crchar + !buff.isEmpty() } def "clear after modify is empty"() { given: - def buff = new LogEventBuffer([:]) + def buff = new LogEventBuffer(null,{[:]}) buff.baos.write('abc'.bytes) - buff.crchar = true + when: buff.clear() then: @@ -66,17 +66,16 @@ class LogEventBufferSpec extends Specification { 0 == buff.baos.size() null == buff.context null == buff.time - !buff.crchar buff.isEmpty() } def "create event with data"() { - def buff = new LogEventBuffer([abc: 'xyz']) + def buff = new LogEventBuffer(LogLevel.DEBUG,{[abc: 'xyz']}) buff.baos.write('abc'.bytes) when: - def log = buff.createEvent(LogLevel.DEBUG) + def log = buff.get() then: null != log @@ -109,11 +108,11 @@ class LogEventBufferSpec extends Specification { def "create event with charset #charset"() { given: - def buff = new LogEventBuffer([:], charset ? Charset.forName(charset) : null) + def buff = new LogEventBuffer(LogLevel.DEBUG,{[:]}, charset ? Charset.forName(charset) : null) buff.baos.write(bytes) when: - def log = buff.createEvent(LogLevel.DEBUG) + def log = buff.get() then: null != log diff --git a/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/ThreadBoundLogOutputStreamSpec.groovy b/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/ThreadBoundLogOutputStreamSpec.groovy deleted file mode 100644 index 657fcbd6a84..00000000000 --- a/rundeckapp/src/test/groovy/com/dtolabs/rundeck/app/internal/logging/ThreadBoundLogOutputStreamSpec.groovy +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2016 SimplifyOps, Inc. (http://simplifyops.com) - * - * 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.dtolabs.rundeck.app.internal.logging - -import com.dtolabs.rundeck.core.execution.Contextual -import com.dtolabs.rundeck.core.logging.LogLevel -import com.dtolabs.rundeck.core.logging.StreamingLogWriter -import com.dtolabs.rundeck.core.utils.ThreadBoundOutputStream -import spock.lang.Specification - -/** - * Created by greg on 2/18/16. - */ -class ThreadBoundLogOutputStreamSpec extends Specification { - def "write without newline"() { - given: - StreamingLogWriter writer = Mock(StreamingLogWriter) - Contextual context = Mock(Contextual) - ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream(writer, LogLevel.DEBUG, context) - - - when: - stream.write('abc no newline'.bytes) - - then: - 0 * writer.addEvent(_) - 1 * context.getContext() - - } - - def "write multiple without newline"() { - given: - StreamingLogWriter writer = Mock(StreamingLogWriter) - Contextual context = Mock(Contextual) - ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream(writer, LogLevel.DEBUG, context) - - - when: - stream.write('abc no newline'.bytes) - stream.write(' still not'.bytes) - stream.write(' more not'.bytes) - - then: - 0 * writer.addEvent(_) - 1 * context.getContext() - - } - - def "write multiple with flush without newline"() { - given: - StreamingLogWriter writer = Mock(StreamingLogWriter) - Contextual context = Mock(Contextual) - ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream(writer, LogLevel.DEBUG, context) - - - when: - stream.write('abc no newline'.bytes) - stream.flush() - stream.write(' still not'.bytes) - stream.flush() - stream.write(' more not'.bytes) - stream.flush() - - then: - 0 * writer.addEvent(_) - 1 * context.getContext() - - } - - def "write with newline"() { - given: - StreamingLogWriter writer = Mock(StreamingLogWriter) - Contextual context = Mock(Contextual) - ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream(writer, LogLevel.DEBUG, context) - - when: - stream.write('abc yes newline\n'.bytes) - - then: - 1 * writer.addEvent(_) - 1 * context.getContext() - } - - def "write multi then newline"() { - given: - StreamingLogWriter writer = Mock(StreamingLogWriter) - Contextual context = Mock(Contextual) - ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream(writer, LogLevel.DEBUG, context) - - when: - stream.write('no newline'.bytes) - stream.write(' still not'.bytes) - stream.write(' more not'.bytes) - stream.write(' then\n'.bytes) - - then: - 1 * writer.addEvent(_) - 1 * context.getContext() - } - - def "write without newline then close"() { - given: - StreamingLogWriter writer = Mock(StreamingLogWriter) - Contextual context = Mock(Contextual) - ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream(writer, LogLevel.DEBUG, context) - - when: - stream.write('no newline'.bytes) - stream.close() - - then: - 1 * writer.addEvent(_) - 1 * context.getContext() - } - - def "write multi without newline then close"() { - given: - StreamingLogWriter writer = Mock(StreamingLogWriter) - Contextual context = Mock(Contextual) - ThreadBoundLogOutputStream stream = new ThreadBoundLogOutputStream(writer, LogLevel.DEBUG, context) - - when: - stream.write('no newline'.bytes) - stream.write(' still not'.bytes) - stream.write(' more not'.bytes) - stream.close() - - then: - 1 * writer.addEvent(_) - 1 * context.getContext() - } - -} diff --git a/rundeckapp/src/test/groovy/rundeck/services/ExecutionUtilServiceTests.groovy b/rundeckapp/src/test/groovy/rundeck/services/ExecutionUtilServiceTests.groovy index 40a30eb1eff..ce661a24e78 100644 --- a/rundeckapp/src/test/groovy/rundeck/services/ExecutionUtilServiceTests.groovy +++ b/rundeckapp/src/test/groovy/rundeck/services/ExecutionUtilServiceTests.groovy @@ -16,7 +16,6 @@ package rundeck.services -import com.dtolabs.rundeck.app.internal.logging.ThreadBoundLogOutputStream import groovy.mock.interceptor.MockFor import groovy.mock.interceptor.StubFor @@ -581,4 +580,4 @@ class MockForThreadOutputStream extends ThreadBoundOutputStream{ void close() throws IOException { } -} \ No newline at end of file +} diff --git a/rundeckapp/src/test/groovy/rundeck/services/LoggingServiceTests.groovy b/rundeckapp/src/test/groovy/rundeck/services/LoggingServiceTests.groovy index f494ce6f0de..abfe92e46db 100644 --- a/rundeckapp/src/test/groovy/rundeck/services/LoggingServiceTests.groovy +++ b/rundeckapp/src/test/groovy/rundeck/services/LoggingServiceTests.groovy @@ -16,11 +16,11 @@ package rundeck.services +import com.dtolabs.rundeck.core.utils.ThreadBoundLogOutputStream import groovy.mock.interceptor.MockFor import static org.junit.Assert.* -import com.dtolabs.rundeck.app.internal.logging.ThreadBoundLogOutputStream import com.dtolabs.rundeck.core.logging.LogEvent import com.dtolabs.rundeck.core.logging.LogLevel import com.dtolabs.rundeck.core.logging.StreamingLogReader @@ -615,7 +615,7 @@ class LoggingServiceTests { public testCreateLogOutputStream() { LoggingService svc = new LoggingService() - def stream = svc.createLogOutputStream(new testWriter(), LogLevel.NORMAL, null) + def stream = svc.createLogOutputStream(new testWriter(), LogLevel.NORMAL, null, null) assertNotNull(stream) assert stream instanceof ThreadBoundLogOutputStream }