Skip to content

Commit ad500f7

Browse files
authored
Concurrency mode (#48)
* Initial implementation of concurrency mode. * Add ability to set each writers concurrency mode individually. * Adding a mutex around print since we can now be called on multiple threads. * Removed .swift-version file and moved version into podspec.
1 parent bb5f813 commit ad500f7

14 files changed

+495
-131
lines changed

.swift-version

Lines changed: 0 additions & 1 deletion
This file was deleted.

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ All significant changes to this project will be documented in this file.
44
## [4.0.0-beta.1](https://github.com/tonystone/tracelog/tree/4.0.0-beta.1)
55

66
#### Added
7-
- `UnifiedLoggingWriter` for Apple Unified Logging system logging using TraceLog.
7+
- Added `UnifiedLoggingWriter` for Apple Unified Logging system logging using TraceLog.
8+
- Added mode to TraceLog.configuration to allow direct, async, or sync mode of operation. Sync & direct mode are useful for use cases that have short-lived processes (scripts) or require real-time logging.
9+
- Added ability to set the concurrency mode individually for each Writer.
810

911
#### Removed
1012
- Removed all Xcode projects, Xcode projects are now generated using Swift Package Manager.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
///
2+
/// ConcurrencyMode+Extensions.swift
3+
///
4+
/// Copyright 2018 Tony Stone
5+
///
6+
/// Licensed under the Apache License, Version 2.0 (the "License");
7+
/// you may not use this file except in compliance with the License.
8+
/// You may obtain a copy of the License at
9+
///
10+
/// http://www.apache.org/licenses/LICENSE-2.0
11+
///
12+
/// Unless required by applicable law or agreed to in writing, software
13+
/// distributed under the License is distributed on an "AS IS" BASIS,
14+
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
/// See the License for the specific language governing permissions and
16+
/// limitations under the License.
17+
///
18+
/// Created by Tony Stone on 5/12/18.
19+
///
20+
import Swift
21+
22+
///
23+
/// The system wide modes that TraceLog can run in. Used to configure a mode globally at configure time.
24+
///
25+
public enum ConcurrencyMode {
26+
27+
///
28+
/// Direct, as the name implies, will directly call the writer from
29+
/// the calling thread with no indirection. It will block until the
30+
/// writer(s) in this mode have completed the write to the endpoint.
31+
///
32+
/// Useful for scripting applications and other applications where
33+
/// it is required for the call not to return until the message is
34+
/// printed.
35+
///
36+
case direct
37+
38+
///
39+
/// Synchronous blocking mode is simaler to direct in that it blocks
40+
/// but this mode also uses a queue for all writes. The benifits of
41+
/// that is that all threads writing to the log will be serialized
42+
/// through before calling the writer (one call to the writer at a
43+
/// time).
44+
///
45+
case sync
46+
47+
///
48+
/// Asynchronous non-blocking mode. A general mode used for most
49+
/// application which queues all messages before being evaluated or logged.
50+
/// This ensures minimal delays in application execution due to logging.
51+
///
52+
case async
53+
54+
///
55+
/// The default mode used if no mode is specified (.async).
56+
///
57+
case `default`
58+
}
59+
60+
///
61+
/// Mode to run a specific Writer in. Used to wrap a writer to change the specific mode it operates in.
62+
///
63+
public enum WriterConcurrencyMode {
64+
65+
///
66+
/// Direct, as the name implies, will directly call the writer from
67+
/// the calling thread with no indirection. It will block until the
68+
/// writer(s) in this mode have completed the write to the endpoint.
69+
///
70+
/// Useful for scripting applications and other applications where
71+
/// it is required for the call not to return until the message is
72+
/// printed.
73+
///
74+
case direct(Writer)
75+
76+
///
77+
/// Synchronous blocking mode is simaler to direct in that it blocks
78+
/// but this mode also uses a queue for all writes. The benifits of
79+
/// that is that all threads writing to the log will be serialized
80+
/// through before calling the writer (one call to the writer at a
81+
/// time).
82+
///
83+
case sync(Writer)
84+
85+
///
86+
/// Asynchronous non-blocking mode. A general mode used for most
87+
/// application which queues all messages before being evaluated or logged.
88+
/// This ensures minimal delays in application execution due to logging.
89+
///
90+
case async(Writer)
91+
}
92+
93+
///
94+
/// Internal ConcurrencyMode extension.
95+
///
96+
internal extension ConcurrencyMode {
97+
98+
///
99+
/// Internal func to convert a `ConcurrencyMode` to a `WriterConcurrencyMode`.
100+
///
101+
internal func writerMode(for writer: Writer) -> WriterConcurrencyMode {
102+
switch self {
103+
case .direct: return .direct(writer)
104+
case .sync: return .sync(writer)
105+
default: return .async(writer)
106+
}
107+
}
108+
}
109+
110+
///
111+
/// Internal WriterConcurrencyMode extension.
112+
///
113+
internal extension WriterConcurrencyMode {
114+
115+
func proxy() -> Writer {
116+
switch self {
117+
case .direct(let writer): return writer
118+
case .sync(let writer): return SynchronousWriterProxy(writer: writer)
119+
case .async(let writer): return AsynchronousWriterProxy(writer: writer)
120+
}
121+
}
122+
}

Sources/TraceLog/Configuration.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@
1919
///
2020
import Foundation
2121

22-
///
23-
/// Internal Configuration data
24-
///
25-
internal class Configuration {
22+
extension Configuration {
2623

2724
enum ConfigurationError: Error, CustomStringConvertible {
2825
case invalidLogLevel(String)
@@ -38,19 +35,29 @@ internal class Configuration {
3835
private static let logTag = "LOG_TAG_"
3936
private static let logPrefix = "LOG_PREFIX_"
4037
private static let logAll = "LOG_ALL"
38+
}
4139

42-
var globalLogLevel = LogLevel.info
40+
///
41+
/// Internal Configuration data
42+
///
43+
internal class Configuration {
4344

44-
var loggedPrefixes: [String: LogLevel] = [:]
45-
var loggedTags: [String: LogLevel] = [:]
46-
var writers: [Writer] = [ConsoleWriter()]
45+
var globalLogLevel: LogLevel = .info
46+
var loggedPrefixes: [String: LogLevel] = [:]
47+
var loggedTags: [String: LogLevel] = [:]
48+
var writers: [Writer] = []
49+
var errors: [ConfigurationError] = []
4750

48-
init () {}
51+
init(writers: [WriterConcurrencyMode] = [.async(ConsoleWriter())], environment: Environment = Environment()) {
52+
self.writers = writers.map( { $0.proxy() } )
53+
54+
self.errors = self.parse(environment: environment)
55+
}
4956

5057
///
5158
/// (Re)Load this structure with the values for the environment variables
5259
///
53-
func load(environment: Environment) -> [ConfigurationError] {
60+
private func parse(environment: Environment) -> [ConfigurationError] {
5461

5562
var errors = [ConfigurationError]()
5663

Sources/TraceLog/ConsoleWriter.swift

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
/// Created by Tony Stone on 4/23/16.
1919
///
2020
import Foundation
21-
import Dispatch
21+
22+
#if os(OSX) || os(iOS) || os(watchOS) || os(tvOS)
23+
import Darwin
24+
#elseif os(Linux) || os(FreeBSD) || os(PS4) || os(Android) /* Swift 5 support: || os(Cygwin) || os(Haiku) */
25+
import Glibc
26+
#endif
2227

2328
///
2429
/// ConsoleWriter is the default `Writer` in **TraceLog** and writes to stdout.
@@ -29,10 +34,28 @@ import Dispatch
2934
///
3035
public class ConsoleWriter: Writer {
3136

37+
///
38+
/// Low level mutex for locking print since it's not reentrent.
39+
///
40+
private var mutex = pthread_mutex_t()
41+
3242
///
3343
/// Default constructor for this writer
3444
///
35-
public init() {}
45+
public init() {
46+
var attributes = pthread_mutexattr_t()
47+
guard pthread_mutexattr_init(&attributes) == 0
48+
else { fatalError("pthread_mutexattr_init") }
49+
pthread_mutexattr_settype(&attributes, Int32(PTHREAD_MUTEX_RECURSIVE))
50+
51+
guard pthread_mutex_init(&mutex, &attributes) == 0
52+
else { fatalError("pthread_mutex_init") }
53+
pthread_mutexattr_destroy(&attributes)
54+
}
55+
56+
deinit {
57+
pthread_mutex_destroy(&mutex)
58+
}
3659

3760
///
3861
/// Required log function for the logger
@@ -45,12 +68,21 @@ public class ConsoleWriter: Writer {
4568
let message = "\(self.dateFormatter.string(from: date)) \(runtimeContext.processName)[\(runtimeContext.processIdentifier):\(runtimeContext.threadIdentifier)] \(levelString): <\(tag)> \(message)"
4669

4770
///
48-
/// Note: we currently use the calling thread to synchronize knowing that
49-
/// TraceLog calls us in a serial queue. This can cause interleaving
50-
/// of other message in the output should other threads be used to
51-
/// print to the screen.
71+
/// Note: Since we could be called on any thread in TraceLog direct mode
72+
/// we protect the print statement with a low-level mutex.
73+
///
74+
/// Pthreads mutexes were chosen because out of all the methods of synchronization
75+
/// available in swift (queue, dispatch semaphores, etc), pthread mutexes are
76+
/// the lowest overhead and fastest lock.
5277
///
78+
/// We also want to ensure we maintain thread boundaries when in direct mode (avoid
79+
/// jumping threads).
80+
///
81+
pthread_mutex_lock(&mutex)
82+
5383
print(message)
84+
85+
pthread_mutex_unlock(&mutex)
5486
}
5587

5688
///

Sources/TraceLog/Logger.swift

Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ internal final class Logger {
4848
self.processName = process.processName
4949
self.processIdentifier = Int(process.processIdentifier)
5050

51-
#if os(macOS)
51+
#if os(iOS) || os(macOS) || os(watchOS) || os(tvOS)
5252
var threadID: UInt64 = 0
5353

5454
pthread_threadid_np(pthread_self(), &threadID)
5555
self.threadIdentifier = threadID
56-
#else // FIXME: Currently it does not seem like Linux supports the pthread_threadid_np method
56+
#else // FIXME: Linux does not support the pthread_threadid_np function, gettid in s syscall must be used.
5757
self.threadIdentifier = 0
5858
#endif
5959
}
@@ -76,46 +76,25 @@ internal final class Logger {
7676
}
7777

7878
///
79-
/// Serialization queue for init and writing
79+
/// Initialize the config with the default configuration first. The
80+
/// user can re-init this at a later time or simply use the default.
8081
///
81-
fileprivate static let queue = DispatchQueue(label: "tracelog.write.queue")
82-
83-
///
84-
/// Initialize the config with the default configuration
85-
/// read from the environment first. The user can re-init
86-
/// this at a later time or simply use the default.
87-
///
88-
fileprivate static let config = Configuration()
82+
fileprivate static var config = Configuration()
8983

9084
///
9185
/// Configure the logging system with the specified writers and environment
9286
///
93-
class func configure(writers: [Writer]? = nil, environment: Environment? = nil) {
94-
95-
///
96-
/// Note: we use a synchronous call here for the configuration, all
97-
/// other calls must be async in order not to conflict with this one.
98-
///
99-
queue.sync {
100-
101-
if let writers = writers {
102-
self.config.writers = writers
103-
}
87+
class func configure(writers: [WriterConcurrencyMode], environment: Environment) {
10488

105-
if let environment = environment {
89+
self.config = Configuration(writers: writers, environment: environment)
10690

107-
let errors = config.load(environment: environment)
108-
109-
logPrimitive(level: .info, tag: moduleLogName, file: #file, function: #function, line: #line) {
110-
"\(moduleLogName) Configured using: \(config.description)"
111-
}
112-
113-
for error in errors {
91+
logPrimitive(level: .info, tag: moduleLogName, file: #file, function: #function, line: #line) {
92+
"\(moduleLogName) Configured using: \(config.description)"
93+
}
11494

115-
logPrimitive(level: .warning, tag: moduleLogName, file: #file, function: #function, line: #line) {
116-
"\(error.description)"
117-
}
118-
}
95+
for error in self.config.errors {
96+
logPrimitive(level: .warning, tag: moduleLogName, file: #file, function: #function, line: #line) {
97+
"\(error.description)"
11998
}
12099
}
121100
}
@@ -124,25 +103,25 @@ internal final class Logger {
124103
/// Low level logging function for Swift calls
125104
///
126105
class func logPrimitive(level: LogLevel, tag: String, file: String, function: String, line: Int, message: @escaping () -> String) {
127-
128-
/// Capture the context outside the dispatch queue
129-
let runtimeContext = RuntimeContextImpl()
130-
let staticContext = StaticContextImpl(file: file, function: function, line: line)
131-
132106
///
133-
/// All logPrimitive calls are asynchronous
107+
/// Capture the timestamp as early as possible to
108+
/// get the most accruate time.
134109
///
135-
queue.async {
110+
let timestamp = Date().timeIntervalSince1970
111+
112+
/// Copy the config pointer so it does not change while we are using it.
113+
let localConfig = self.config
114+
115+
if localConfig.logLevel(for: tag) >= level {
136116

137-
if config.logLevel(for: tag) >= level {
138-
let timestamp = Date().timeIntervalSince1970
117+
let runtimeContext = RuntimeContextImpl()
118+
let staticContext = StaticContextImpl(file: file, function: function, line: line)
139119

140-
/// Evaluate the message now
141-
let messageString = message()
120+
/// Evaluate the message now
121+
let messageString = message()
142122

143-
for writer in config.writers {
144-
writer.log(timestamp, level: level, tag: tag, message: messageString, runtimeContext: runtimeContext, staticContext: staticContext)
145-
}
123+
for writer in localConfig.writers {
124+
writer.log(timestamp, level: level, tag: tag, message: messageString, runtimeContext: runtimeContext, staticContext: staticContext)
146125
}
147126
}
148127
}

0 commit comments

Comments
 (0)