scryops obs GUIDES

Data Masking in Telemetry: The Art of Safe Transformation

Telemetry data is just as risky for PII as any database. Here's how to turn sensitive fields into safe, useful signals: hashing, tokenising, coarsening, and picking the right tool for the job.

This guide dives straight into the how-to of transforming your data. If you’re wondering which fields are trouble or what compliance wants from you, check out Your Traces Are Leaking User Data. This is about how to actually make your data safe.

applicationscrub before SDKOTel SDKspans + logsCollectorprimary controlbackendresidency + TTLwho queriesaudit trailheavy border = the stage this guide covers
Fig. — Five stages, one control each. The heavy border marks where this guide picks up.

The Data Transformation Pipeline

Masking is a relay. Each stage hands off to the next, trusting the last step did its job. Miss a handoff and you leak fields you thought were safe.

rawtelemetryneedsmasking?noyespass throughtransformone per fieldhash · tokenisetruncate · aggregatequalitygatepassexportfaildashed return = the failed record goes back to transform, never forward. a gate, not a report.
Fig. — The four techniques are a menu, not a queue. Each field picks one.
  1. Raw Telemetry: The raw, sensitive data as it’s initially collected. It contains PII, and if you export it unchanged, it lands in your trace backend indexed and searchable: a GDPR audit waiting to happen.

  2. Masking Decision: This is where you sort the fields. Some need redaction. Others, like system metrics or anonymous usage stats, carry no personal identifiers and go through untouched.

  3. Transformation: This is where the actual masking happens, matched to each field’s type and sensitivity:

    • Hashing: Run sensitive data, like user IDs or email addresses, through a one-way function, and you get a fixed-length string. You can’t get the original back, but you can still analyse and correlate.
    • Tokenisation: Swap the sensitive data for a random, unique token instead, and keep a secure lookup table that maps tokens back to original values. Only authorised systems that need re-identification get access to that table.

Which of the two you reach for is decided by the field, not by preference, and getting it wrong is the most common failure in this whole area:

A hash is only as strong as its input's search space
bar length = log₁₀ of the candidate count · the exponent is printed because the ratio does not fit on a page
email address≈ 10⁹ · 4.6 billion accounts
Exhaustible in under a second on one GPU. Hashing an email produces a pseudonym anyone can reverse with a wordlist. Delete it instead.
opaque business ID + salt≈ 10²⁰ with a secret salt
The case where hashing genuinely works — provided the salt is secret and rotated. Order IDs and transaction IDs live here.
random 128-bit token≈ 10³⁸ · 2¹²⁸
Tokenisation, not hashing. The mapping lives in a separate store you control, and nothing in the telemetry can reverse it.
# the whole argument, in three lines
candidates(email) ≈ 4.6e9 # every email account on earth
gpu_sha256_per_sec ≈ 2e10 # order of magnitude, one card
time_to_exhaust ≈ 0.23 s # 4.6e9 / 2e10
SHA-256 is not the weak part. The email is. The same hash over a 128-bit random token is unbreakable, over an email address it is a lookup. This is why the field-level decision comes before the algorithm choice, every time.
Fig. — “We hashed it” is an answer to the wrong question.

Hashing works when the input space is large enough that an attacker can’t enumerate it. An opaque order ID with a secret salt qualifies. An email address doesn’t: there are only so many email addresses in the world, and a single GPU walks the entire list in under a second. Personal identifiers get deleted or tokenised. Business identifiers get hashed.

Transformation Examples

User Activity Telemetry

Before transformation:

{
  "event": "user_login",
  "timestamp": "2024-02-15T10:30:00Z",
  "attributes": {
    "user.email": "sarah.jones@company.com",
    "user.ip": "192.168.1.100",
    "device.id": "d789-xyz-456",
    "location": "San Francisco, CA",
    "browser": "Chrome 120.0.0",
    "login_success": true
  }
}

After transformation:

{
  "event": "user_login",
  "timestamp": "2024-02-15T10:30:00Z",
  "attributes": {
    "user.id": "<hash_value>",
    "user.ip_prefix": "192.168.0.0/16",
    "device.type": "web_browser",
    "location.region": "US-WEST",
    "browser.family": "Chrome",
    "login_success": true
  }
}

Transformation Patterns

graph LR A[/"Data input"/] --> B(Identifiers) A --> C(Locations) A --> D(Metrics) A --> E(Timestamps) B --> B1[Hash] C --> C1[Generalize] D --> D1[Round] E --> E1[Bucket] style A fill:#1C1C1C,stroke:#3A6FAF,color:#5B8DEF,stroke-width:1.5px,stroke-dasharray:2 2 classDef second fill:#161616,stroke:#3A6FAF,color:#5B8DEF,stroke-width:1.5px,stroke-dasharray:2 2 classDef third fill:#1C1C1C,stroke:#2A2A2A,color:#A8A8A0 class B,C,D,E second; class B1,C1,D1,E1 third;
Fig. — Each data type gets its own transformation: identifiers are hashed, locations are generalized, metrics are rounded, and timestamps are bucketed.

Quality Control Gates

Each gate checks a structural property of the transformed data before it reaches the exporter:

THREE CHECKS  ·  ALL MUST PASS  ·  PER RECORD
format check
is the field still the type the schema promises?
an int coarsened into "US-WEST" breaks every downstream aggregation
pattern check
does anything still match a PII regex?
the check that catches an email that arrived inside a free-text field
value check
is the value inside the range the transform promised?
a hash that came back empty, a bucket that landed outside its own boundaries
all three pass → export
any one fails → back to transform
The join is an AND, and that is the design decision worth naming: one bad field fails the whole record. Partially masked telemetry is worse than none — it looks clean, so nobody checks it again.
Fig. — A gate that lets a record through on two out of three is a suggestion.

Transformation Matrix

Data TypeExampleTransformationRationaleResult Example
Emailuser@company.comRemovePII — no safe hash(deleted)
IP Address192.168.1.100Subnet MaskNetwork analysis192.168.0.0/16
LocationSan Francisco, CARegion CodeGeographic trendsUS-WEST
Timestamp2024-02-15T10:30:00ZTime BucketPattern analysis2024-02-15T10:00:00Z

Data Utility Preservation

The transformation must preserve the relationships between fields: statistical distributions, cross-span correlations, and time-series patterns. Lose those, and the data loses its diagnostic value:

graph TB A[Data Value] --> B[Statistical] A --> C[Relational] A --> D[Temporal] B --> B1[Distributions] B --> B2[Aggregates] C --> C1[Dependencies] C --> C2[Hierarchies] D --> D1[Sequences] D --> D2[Patterns]
Fig. — A transformation has to preserve three kinds of structure at once: statistical distributions, relationships between fields, and temporal sequence, or the masked data loses its diagnostic value.

Common Pitfalls and Solutions

These are the two failures that break pipelines in practice, over and over:

  1. Inconsistent Masking

    // Bad: Same value masked differently
    {
      "user_id": "hash1",
      "referenced_user": "hash2"  // Same user, different hash!
    }
    
    // Good: Consistent masking
    {
      "user_id": "hash1",
      "referenced_user": "hash1"  // Same user, same hash
    }
    
  2. Over-Masking

    // Bad: Losing analytical value
    {
      "region": "****",
      "response_time_ms": "****"  // Don't mask metrics!
    }
    
    // Good: Preserve useful data
    {
      "region": "US-WEST",
      "response_time_ms": 123
    }
    
A good masking process turns sensitive data into safe, useful signals. The trick is picking the right transformation for each data type and sticking with it all the way through your telemetry pipeline.
Friar Cluck CLERIC · keeper of the postmortem

I have anointed your logs with the holy redaction. The secrets are sealed, the PII is at rest. Go forth and ship.

The Cucco — the party's most expendable adventurer.

See Also