Keep private information out of your logs with Swift

One simple trick to avoid unintentionally leaking your users private info

Oleg Dreyman
4 min readDec 22, 2020

So imagine you have an app with a fairly simple model struct like this:

struct User {
var identifier: String
var handle: String
var name: String
var dateOfBirth: Date
var city: String
}

And to help you debug problems in your code, you have a remote-logging system that will send error messages to your backend (say, Sentry or something similar). The function looks like this:

func logError(_ description: String, userInfo: Any...) {
// ...
}

And then, whenever something fishy is happening, you use the logError function so that you can investigate it later:

logError("trying to edit the user that is not current user")

But wait! This message alone is not that useful. So to help you pinpoint exactly what’s happening, you usually include your model objects as well:

logError("trying to edit the user that is not current user", user, currentUser)

Wonderful, isn’t it?

…Well, there’s a problem

Open your logs, and here’s what you’re likely to see alongside your error messages:

▿ Your_App.User
- identifier: "6EEBA88F-D397-4108-B3D9-2B60E5FB4477"
- handle: "swifty15"
- name: "John Appleseed"
▿ dateOfBirth: 1976-01-04 20:36:07 +0000
- timeIntervalSinceReferenceDate: -788671432.408519
- city: "Oakland"

Yikes. All this sensitive data like name, city and date of birth, is leaked to your logs now. Funny thing is that it’s really not something you even need in this case, but still something that you’re totally responsible for.

Don’t commit your users’ personal information to your logs!

But how, you would ask? Should I go over every single logError line in my code and make sure that I only submit the data I need? Should I now worry about it every time I write another log statement? How am I gonna make sure my teammates don’t make the same mistake?

Solution

Luckily, there’s a simple universal solution that can help you solve this problem once and for all. And it involves… 🥁

Property wrappers! Yay, not only are we making our code safer for our users, but also much, much trendier for our developers. Win-win!

So, here’s a neat little property wrapper for this:

@propertyWrapper
struct LoggingExcluded<Value>: CustomStringConvertible, CustomDebugStringConvertible, CustomLeafReflectable {

var wrappedValue: Value

init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}

var description: String {
return "--redacted--"
}

var debugDescription: String {
return "--redacted--"
}

var customMirror: Mirror {
return Mirror(reflecting: "--redacted--")
}
}

This wrapper doesn't do much: it simply makes sure that when your code is trying to print , debugPrint or dump a property, it will show --redacted-- instead of an actual value. And since virtually any existing logging system uses one of these functions, it will effectively remove any @LoggingExcluded property from our logs.

So let’s update our User struct:

struct User {
var identifier: String
var handle: String
@LoggingExcluded var name: String
@LoggingExcluded var dateOfBirth: Date
@LoggingExcluded var city: String
}

And to make sure it works as expected, let’s test our User instance with each of the three logging functions: print , debugPrint and dump :

// print
User(identifier: "6EEBA88F-D397-4108-B3D9-2B60E5FB4477", handle: "swifty15", _name: --redacted--, _dateOfBirth: --redacted--, _city: --redacted--)
// debugPrint
Your_App.User(identifier: "6EEBA88F-D397-4108-B3D9-2B60E5FB4477", handle: "swifty15", _name: --redacted--, _dateOfBirth: --redacted--, _city: --redacted--)
// dump
▿ Your_App.User
- identifier: "6EEBA88F-D397-4108-B3D9-2B60E5FB4477"
- handle: "swifty15"
- _name: --redacted--
- _dateOfBirth: --redacted--
- _city: --redacted--

Works absolutely flawlessly!

So there you have it — just prefix your property with @LoggingExcluded when you want to keep some information out of your logs, and it will never show up in your logs no matter what you do.

I’ve tried this approach in both “Ask Yourself Everyday” and “Time and Again”, and it worked perfectly for me. Now I always sleep peacefully knowing that my users’ data is absolutely safe.

Does this work for you? Do you already use a similar pattern in your code? Have you tried implementing this but for some reason it’s not suitable for your codebase? Or do you have any other questions about this? Let me know in the comments section below! I’m truly excited to hear from you.

Appendix 1: Adding Codable conformance

extension LoggingExcluded: Decodable where Value: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.wrappedValue = try container.decode(Value.self)
}
}
extension LoggingExcluded: Encodable where Value: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
}

Thanks for reading the post! Don’t hesitate to ask or suggest anything in the “responses” section below. You can also contact me on Twitter or find me on GitHub. If you have written a piece (or stumbled upon one) exploring similar topic — make sure to post a link to it in the responses so I can include it right below.

Further learning:

Hi! If you want to support me, please check out my apps: Ask Yourself Everyday, Time and Again and Women’s Football 2017. For business inquiries, reach me at oleg@dreyman.dev. Thanks for reading!

--

--

Oleg Dreyman

iOS development know-it-all. Talk to me about Swift, coffee, photography & motorsports.