Skip to content

Commit

Permalink
FileWriter rewrite (#85)
Browse files Browse the repository at this point in the history
* Add RawOutputStream to handle the low-level output.
* Initial rewrite of FileWriter
  - Added FileStrategys `.fixed` and `.rotate` for management of the physical file on disk.
  - Added FileStrategyManager protocol to base implementations on.
  - Added FileStrategyFixed implementation.
  - Added FileStrategyRotate implementation.
  - Cleanup construction method signature making it easier to work with.
  - Added needed tests for all new functionality.
* Change the default of FileWriter format parameter to remove strip control characters.
* Update version to 5.0.0-beta.1 for release.
  • Loading branch information
tonystone authored Feb 24, 2019
1 parent 8d4c0e2 commit 2faa1ea
Show file tree
Hide file tree
Showing 19 changed files with 816 additions and 401 deletions.
18 changes: 9 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
# Change Log
All significant changes to this project will be documented in this file.

## [5.0.0] (Upcoming release)
## [5.0.0-beta.1] (https://github.com/tonystone/tracelog/tree/5.0.0-beta.1)

#### Added
- Added `OutputStreamFormatter` protocol to define formatters for use with byte output stream type Writers.
- Added `TextFormat`, an implementation of a OutputStreamFormatter that formats its output based on a supplied template (this is the default formatter for Console and File output).
- Added `JSONFormat`, an implementation of a OutputStreamFormatter that formats its output in standard JSON format.
- Added `OutputStreamWriter` protocol to define types that write byte streams to their output and accept `OutputStreamFormatter` types to format the output.
- Added `LogEntry` tuple type to `Writer` defining the formal types that a Writer writes.

#### Added
- Added `.buffer` option for `.async` concurrency modes to allow for buffering when the writer is not available to write to its endpoint.

#### Changed
Expand All @@ -19,16 +17,18 @@ All significant changes to this project will be documented in this file.
- Changed `Writer` protocol `log()` method to `write(_ entry: Writer.LogEntry)` to make it easier to process messages by writers and formatters.
- Changed `Writer` return to `Swift.Result<Int, FailedReason>` to return instructions for TraceLog for buffering and error recovery.
- Changed `ConsoleWriter` to accept new `OutputStreamFormatter` instances allowing you to customize the output log format (default is `TextFormat`.)
- Changed `FileWriter` to accept new `OutputStreamFormatter` instances allowing you to customize the output log format (default is `TextFormat`.)
- Changed `FileWriter` archive file name date format to "yyyyMMdd-HHmm-ss-SSS".
* This was done for maximum compatibility between platforms and can be overridden in the FileConfiguration object passed at init.
- Changed `FileWriter` public interface
* `FileWriter` now requires the log directory be passed in, removing default value of `./`.
* Removed the `fileConfiguration` parameter replacing with new `strategy` enum.
* It now accepts the new `OutputStreamFormatter` instances allowing you to customize the output log format (default is `TextFormat`.)
- Changed `FileWriter` archive file name date format to "yyyyMMdd-HHmm-ss-SSS" (This was done for maximum compatibility between platforms and can be overridden in the FileConfiguration object passed at init.)

#### Removed
- Removed `TraceLogTestHarness` module.

#### Fixed
- Fixed `logTrace` when no trace level is passed. It's now the correct default value of 1 instead of 4 (issue #58).

#### Removed
- Removed `TraceLogTestHarness` module.

## [4.0.1](https://github.com/tonystone/tracelog/tree/4.0.1)

#### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
source 'https://rubygems.org'
gem 'cocoapods', '1.6.0.beta.2'
gem 'cocoapods', '1.6.0'
15 changes: 5 additions & 10 deletions Sources/TraceLog/Internal/Utilities/Streams/FileOutputStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import Foundation
/// interface. At this writing TraceLog only requires writing of
/// files therefore, only the output portion was implemented.
///
/// - Remark: Why note just use `Foundation.FileHandle`? We found
/// - Remark: Why not just use `Foundation.FileHandle`? We found
/// that the FileHandle implementation at this writing is
/// not suitable for TraceLog's stringent fault tolerance
/// requirements. The current implementation of the
Expand Down Expand Up @@ -97,13 +97,6 @@ internal class FileOutputStream: RawOutputStream {
///
let url: URL

/// TODO: Remove when FileWriter rewrite is complete.
internal init(fileDescriptor: Int32, closeFd: Bool, url: URL) {
self.url = url

super.init(fileDescriptor: fileDescriptor, closeFd: closeFd)
}

/// Initialize an instance of self for the file at URL.
///
/// Files opened with this method are always opened for WRITE ONLY
Expand All @@ -118,7 +111,7 @@ internal class FileOutputStream: RawOutputStream {
///
/// - SeeAlso: FileOutputStreamError
///
convenience init(url: URL, options: OpenOptions = [], mode: Mode = [.readUser, .writeUser, .readGroup, .readOther]) throws {
init(url: URL, options: OpenOptions = [], mode: Mode = [.readUser, .writeUser, .readGroup, .readOther]) throws {

/// Open the file at the URL for write and append since this
/// is specifically an OutputStream.
Expand All @@ -128,7 +121,9 @@ internal class FileOutputStream: RawOutputStream {
guard descriptor != -1
else { throw FileOutputStreamError.error(for: errno) }

self.init(fileDescriptor: descriptor, closeFd: true, url: url)
self.url = url

super.init(fileDescriptor: descriptor, closeFd: true)
}

/// The current write position or number of bytes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,6 @@ extension RawOutputStream: OutputStream {
}
}

extension RawOutputStream: Equatable {
static func == (lhs: RawOutputStream, rhs: RawOutputStream) -> Bool {
return lhs.fd == rhs.fd
}
}

/// Private extension to work around Swifts confusion around similar function names.
///
internal extension RawOutputStream {
Expand Down
40 changes: 40 additions & 0 deletions Sources/TraceLog/Writers & Formatters/FileStrategy+Fixed.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
///
/// FileStrategy+Fixed.swift
///
/// Copyright 2019 Tony Stone
///
/// 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.
///
/// Created by Tony Stone on 1/31/19.
///
import Foundation

internal class FileStrategyFixed: FileStrategyManager {

let stream: FileOutputStream

var url: URL {
return self.stream.url
}

init(directory: URL, fileName: String) throws {
self.stream = try FileOutputStream(url: directory.appendingPathComponent(fileName), options: [.create])
}
deinit {
self.stream.close()
}

func write(_ bytes: [UInt8]) -> Result<Int, FailureReason> {
return self.stream.write(bytes).mapError({ self.failureReason($0) })
}
}
189 changes: 189 additions & 0 deletions Sources/TraceLog/Writers & Formatters/FileStrategy+Rotate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
///
/// FileStrategyRotate.swift
///
/// Copyright 2019 Tony Stone
///
/// 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.
///
/// Created by Tony Stone on 2/1/19.
///
import CoreFoundation
import Foundation

internal class FileStrategyRotate: FileStrategyManager {

/// The current url in use or if none open yet, the one that will be used.
///
var url: URL {
return self.stream.url
}

/// Initialize the FileStrategy with the directory for files, the template for
/// file naming, and the options for rotation.
///
/// - Parameters:
/// - directory: The directory URL that the strategy will write files to.
/// - template: The naming template to use for naming files.
/// - options: The rotation options to use for file rotation.
///
init(directory: URL, template: String, options: Set<FileStrategy.RotateOption>) throws {

var rotate: (onStartup: Bool, maxSize: UInt64?) = (false, nil)

for option in options {
switch option {
case .startup: rotate.onStartup = true
case .maxSize(let maxSize): rotate.maxSize = maxSize
}
}
let fileStreamManager = FileStreamManager(directory: directory, template: template)

/// Open the file for writing.
self.stream = rotate.onStartup ? try fileStreamManager.openNewFileStream() : try fileStreamManager.openLatestFileStream()

self.fileStreamManager = fileStreamManager
self.rotate = rotate
self.mutex = Mutex(.normal)
}

/// Required implementation for FileStrategyManager classes.
///
func write(_ bytes: [UInt8]) -> Result<Int, FailureReason> {

/// Note: Since we could be called on any thread in TraceLog direct mode
/// we protect the file with a low-level mutex.
///
/// PThreads mutexes were chosen because out of all the methods of synchronization
/// available in swift (queue, dispatch semaphores, etc), PThread mutexes are
/// the lowest overhead and fastest lock.
///
/// We also want to ensure we maintain thread boundaries when in direct mode (avoid
/// jumping threads).
///
mutex.lock(); defer { mutex.unlock() }

/// Does the file need to be rotated?
if let maxSize = self.rotate.maxSize, self.stream.position + UInt64(bytes.count) >= maxSize {
do {
/// Open the new file first so that if we get an error, we can leave the old stream as is
let newStream = try fileStreamManager.openNewFileStream()

self.stream.close()
self.stream = newStream
} catch {
return .failure(.error(error))
}
}
return self.stream.write(bytes).mapError({ self.failureReason($0) })
}

/// Rotation options.
///
private let rotate: (onStartup: Bool, maxSize: UInt64?)

/// File configuration for naming file.
///
private let fileStreamManager: FileStreamManager

/// The outputStream to use for writing.
///
private var stream: FileOutputStream

/// Low level mutex for locking print since it's not reentrant.
///
private let mutex: Mutex
}

/// Represents log file configuration settings
///
internal /* @testable */
struct FileStreamManager: Equatable {

internal init(directory: URL, template: String) {
self.directory = directory
self.nameFormatter = DateFormatter()
self.nameFormatter.dateFormat = template

let metaDirectory = directory.appendingPathComponent(".tracelog", isDirectory: true)

self.metaDirectory = metaDirectory
self.metaFile = metaDirectory.appendingPathComponent("filewriter.meta", isDirectory: false)
}

/// Open the output stream creating the meta file when created
///
internal func openNewFileStream() throws -> FileOutputStream {
let newURL = self.newFileURL()

try writeMetaFile(for: newURL)

return try FileOutputStream(url: newURL, options: [.create])
}

/// Find the latest log file by creation date that exists.
///
/// - Returns: a URL if there is an existing file otherwise, nil.
///
internal func openLatestFileStream() throws -> FileOutputStream {
let latestURL = latestFileURL()

try writeMetaFile(for: latestURL)

return try FileOutputStream(url: latestURL, options: [.create])
}

/// Create a file URL based on the files configuration.
///
internal /* @testable */
func newFileURL() -> URL {
return self.directory.appendingPathComponent(self.nameFormatter.string(from: Date()))
}

/// Find the latest log file by creation date that exists.
///
/// - Returns: a URL if there is an existing file otherwise, nil.
///
internal /* @testable */
func latestFileURL() -> URL {

guard let latestPath = try? String(contentsOf: self.metaFile, encoding: .utf8)
else { return self.newFileURL() }

return URL(fileURLWithPath: latestPath, isDirectory: false)
}

/// Writes a meta file out for the url passed.
///
private func writeMetaFile(for url: URL) throws {

/// Write the file that contains the last path.
do {
if !FileManager.default.fileExists(atPath: self.metaDirectory.path) {
try FileManager.default.createDirectory(at: self.metaDirectory, withIntermediateDirectories: true)
}

try url.path.write(to: self.metaFile, atomically: false, encoding: .utf8)
} catch {
throw FileOutputStreamError.unknownError(0, error.localizedDescription)
}
}


private let directory: URL

private var metaDirectory: URL

private var metaFile: URL

private let nameFormatter: DateFormatter
}
Loading

0 comments on commit 2faa1ea

Please sign in to comment.