-
Notifications
You must be signed in to change notification settings - Fork 103
Extending HDFS Lineage Dispatcher to support writing lineage files to a centralised location #893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 1 commit
8d49e2f
a35a99c
7a824bf
96373b4
2b01b81
6f0ecdf
b064041
578c876
bebf4c9
04eebe3
5f63f61
8bb78a1
7a93705
32db7ac
30f2f40
87936b8
4e699dd
8a4b431
6bc068d
d155a98
4c7d723
cd535c9
0e5c69a
bebc869
7bd5b0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…sing internal SparkContext values
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -51,9 +51,9 @@ import scala.concurrent.blocking | |||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * 2. CENTRALIZED MODE (customLineagePath set to a valid path): | ||||||||||||||||||||||||||||||||||||||
| * All lineage files are written to a single centralized location with unique filenames. | ||||||||||||||||||||||||||||||||||||||
| * Filename format: {timestamp}_{fileName}_{appId} | ||||||||||||||||||||||||||||||||||||||
| * Filename format: {timestamp}_{appName}_{appId} | ||||||||||||||||||||||||||||||||||||||
| * - timestamp: Human-readable UTC timestamp (yyyy-MM-dd_HH-mm-ss-SSS) for chronological sorting and filtering | ||||||||||||||||||||||||||||||||||||||
| * - fileName: The configured fileName value (e.g., "my_file.parq_LINEAGE") | ||||||||||||||||||||||||||||||||||||||
| * - appName: Spark application name for easy identification | ||||||||||||||||||||||||||||||||||||||
| * - appId: Spark application ID for traceability | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * The timestamp-first format ensures natural chronological sorting and easy date-based filtering. | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -87,7 +87,7 @@ class HDFSLineageDispatcher(filename: String, permission: FsPermission, bufferSi | |||||||||||||||||||||||||||||||||||||
| throw new IllegalStateException("send(event) must be called strictly after send(plan) method with matching plan ID") | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| val path = resolveLineagePath(event.planId.toString) | ||||||||||||||||||||||||||||||||||||||
| val path = resolveLineagePath() | ||||||||||||||||||||||||||||||||||||||
| val planWithEvent = Map( | ||||||||||||||||||||||||||||||||||||||
| "executionPlan" -> this._lastSeenPlan, | ||||||||||||||||||||||||||||||||||||||
| "executionEvent" -> event | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -108,6 +108,7 @@ class HDFSLineageDispatcher(filename: String, permission: FsPermission, bufferSi | |||||||||||||||||||||||||||||||||||||
| * @return The full path where the lineage file should be written | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| private def resolveLineagePath(): String = { | ||||||||||||||||||||||||||||||||||||||
| val outputSource = s"${this._lastSeenPlan.operations.write.outputSource}" | ||||||||||||||||||||||||||||||||||||||
| customLineagePath match { | ||||||||||||||||||||||||||||||||||||||
| case Some(customPath) => | ||||||||||||||||||||||||||||||||||||||
| // Centralized mode: write to custom path with unique filename | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -116,28 +117,30 @@ class HDFSLineageDispatcher(filename: String, permission: FsPermission, bufferSi | |||||||||||||||||||||||||||||||||||||
| s"$cleanCustomPath/$uniqueFilename" | ||||||||||||||||||||||||||||||||||||||
| case None => | ||||||||||||||||||||||||||||||||||||||
| // Default mode: write alongside target data file | ||||||||||||||||||||||||||||||||||||||
| s"${this._lastSeenPlan.operations.write.outputSource.stripSuffix("/")}/$filename" | ||||||||||||||||||||||||||||||||||||||
| s"${outputSource.stripSuffix("/")}/$filename" | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Generates a unique filename for centralized lineage storage. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * Format: {timestamp}_{fileName}_{appId} | ||||||||||||||||||||||||||||||||||||||
| * Example: 2025-10-12_14-30-45-123_lineage_app-20251012143045-0001 | ||||||||||||||||||||||||||||||||||||||
| * Format: {timestamp}_{appName}_{appId} | ||||||||||||||||||||||||||||||||||||||
| * Example: 2025-10-12_14-30-45-123_MySparkJob_app-20251012143045-0001 | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * This format optimizes for operational debugging use cases: | ||||||||||||||||||||||||||||||||||||||
| * - Timestamp FIRST: Ensures natural chronological sorting (most recent files appear together) | ||||||||||||||||||||||||||||||||||||||
| * - Application Name: Easy identification of which job generated the lineage | ||||||||||||||||||||||||||||||||||||||
| * - Application ID: Full traceability to specific Spark application run | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @return A unique filename optimized for filtering and sorting | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| private def generateUniqueFilename(): String = { | ||||||||||||||||||||||||||||||||||||||
| val sparkContext = SparkContext.getOrCreate() | ||||||||||||||||||||||||||||||||||||||
| val appName = sparkContext.appName | ||||||||||||||||||||||||||||||||||||||
| val appId = sparkContext.applicationId | ||||||||||||||||||||||||||||||||||||||
| val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss-SSS").withZone(ZoneId.of("UTC")) | ||||||||||||||||||||||||||||||||||||||
| val timestamp = dateFormatter.format(Instant.now()) | ||||||||||||||||||||||||||||||||||||||
| s"${timestamp}_${filename}_${appId}" | ||||||||||||||||||||||||||||||||||||||
| s"${timestamp}_${appName}_${appId}" | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+147
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanitize appName and appId to prevent filesystem path issues. The Spark application name can contain spaces, slashes, or special characters that may cause filesystem path issues. While the filename format is well-designed for sorting, unsanitized names can break path construction. Apply this diff to sanitize the application metadata: private def generateUniqueFilename(): String = {
val sparkContext = SparkContext.getOrCreate()
- val appName = sparkContext.appName
- val appId = sparkContext.applicationId
+ val appName = sparkContext.appName.replaceAll("[^a-zA-Z0-9_-]", "_")
+ val appId = sparkContext.applicationId.replaceAll("[^a-zA-Z0-9_-]", "_")
val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss-SSS").withZone(ZoneId.of("UTC"))
val timestamp = dateFormatter.format(Instant.now())
s"${timestamp}_${appName}_${appId}"
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.