Skip to content

Commit 6fdbaa6

Browse files
Merge branch 'dev' into unicron-signatures-per-month-prod
Signed-off-by: Lukasz Gryglicki <[email protected]> Assisted by [OpenAI](https://platform.openai.com/) Assisted by [GitHub Copilot](https://github.com/features/copilot)
2 parents 2d4d152 + 1edadbf commit 6fdbaa6

File tree

2 files changed

+431
-0
lines changed

2 files changed

+431
-0
lines changed
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
// Copyright The Linux Foundation.
2+
// SPDX-License-Identifier: MIT
3+
4+
package main
5+
6+
import (
7+
"encoding/csv"
8+
"fmt"
9+
"log"
10+
"os"
11+
"sort"
12+
"strconv"
13+
"strings"
14+
"time"
15+
16+
"github.com/aws/aws-sdk-go/aws"
17+
"github.com/aws/aws-sdk-go/aws/session"
18+
"github.com/aws/aws-sdk-go/service/dynamodb"
19+
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
20+
)
21+
22+
const (
23+
regionDefault = "us-east-1"
24+
profileName = "lfproduct-prod"
25+
tableName = "cla-prod-signatures"
26+
)
27+
28+
// SignatureRecord represents the DynamoDB signature record structure
29+
type SignatureRecord struct {
30+
SignatureID string `dynamodbav:"signature_id"`
31+
DateCreated string `dynamodbav:"date_created"`
32+
ApproxDateCreated string `dynamodbav:"approx_date_created"`
33+
SignatureType string `dynamodbav:"signature_type"`
34+
SigtypeSignedApprovedID string `dynamodbav:"sigtype_signed_approved_id"`
35+
SignatureApproved bool `dynamodbav:"signature_approved"`
36+
SignatureSigned bool `dynamodbav:"signature_signed"`
37+
}
38+
39+
// MonthlyStats holds the count of signatures per month
40+
type MonthlyStats struct {
41+
Month string
42+
ICLA int
43+
ECLA int
44+
CCLA int
45+
}
46+
47+
// go run cmd/monthly_signature_report/monthly_signature_report.go
48+
func main() {
49+
// Set up AWS session
50+
sess, err := session.NewSessionWithOptions(session.Options{
51+
Profile: profileName,
52+
Config: aws.Config{
53+
Region: aws.String(regionDefault),
54+
},
55+
})
56+
if err != nil {
57+
log.Fatalf("Error creating AWS session: %v", err)
58+
}
59+
60+
svc := dynamodb.New(sess)
61+
62+
fmt.Println("Scanning signatures table for ICLA, ECLA, and CCLA statistics...")
63+
64+
// Monthly counters map[YYYY-MM]Stats
65+
monthlyStats := make(map[string]*MonthlyStats)
66+
67+
// Scan parameters
68+
// Full attributes scan
69+
// params := &dynamodb.ScanInput{TableName: aws.String(tableName)}
70+
// Scan only needed parameters
71+
params := &dynamodb.ScanInput{
72+
TableName: aws.String(tableName),
73+
// Only fetch the fields we actually need
74+
ProjectionExpression: aws.String(
75+
"#sid, #dc, #adc, #st, #ssa, #sa, #ss",
76+
),
77+
ExpressionAttributeNames: map[string]*string{
78+
"#sid": aws.String("signature_id"),
79+
"#dc": aws.String("date_created"),
80+
"#adc": aws.String("approx_date_created"),
81+
"#st": aws.String("signature_type"),
82+
"#ssa": aws.String("sigtype_signed_approved_id"),
83+
"#sa": aws.String("signature_approved"),
84+
"#ss": aws.String("signature_signed"),
85+
},
86+
}
87+
88+
// Get current time for validation
89+
now := time.Now()
90+
currentMonth := now.Format("2006-01")
91+
92+
totalProcessed := 0
93+
totalICLA := 0
94+
totalECLA := 0
95+
totalCCLA := 0
96+
skippedInvalidDates := 0
97+
skippedFutureDates := 0
98+
99+
// Scan the table
100+
err = svc.ScanPages(params, func(page *dynamodb.ScanOutput, lastPage bool) bool {
101+
for _, item := range page.Items {
102+
var sig SignatureRecord
103+
e := dynamodbattribute.UnmarshalMap(item, &sig)
104+
if e != nil {
105+
log.Printf("Error unmarshalling record: %v", e)
106+
continue
107+
}
108+
109+
totalProcessed++
110+
if totalProcessed%1000 == 0 {
111+
fmt.Printf("Processed %d records...\n", totalProcessed)
112+
}
113+
114+
// Only process signatures that are signed and approved
115+
if !sig.SignatureSigned || !sig.SignatureApproved {
116+
continue
117+
}
118+
119+
// Get the creation date (prefer date_created, fallback to approx_date_created)
120+
creationDate := sig.DateCreated
121+
if creationDate == "" {
122+
creationDate = sig.ApproxDateCreated
123+
}
124+
if creationDate == "" {
125+
continue
126+
}
127+
128+
// Parse creation date to extract month
129+
month := extractMonth(creationDate)
130+
if month == "" {
131+
skippedInvalidDates++
132+
continue
133+
}
134+
135+
// Check if month is in the future
136+
// Month and currentMonth are formatted as YYYY-MM, so string comparison is safe
137+
if month > currentMonth {
138+
skippedFutureDates++
139+
continue
140+
}
141+
142+
// Determine signature type based on multiple factors
143+
var isICLA, isECLA, isCCLA bool
144+
145+
// Primary method: check sigtype_signed_approved_id
146+
if sig.SigtypeSignedApprovedID != "" {
147+
if strings.HasPrefix(sig.SigtypeSignedApprovedID, "icla#") {
148+
isICLA = true
149+
totalICLA++
150+
} else if strings.HasPrefix(sig.SigtypeSignedApprovedID, "ecla#") {
151+
isECLA = true
152+
totalECLA++
153+
} else if strings.HasPrefix(sig.SigtypeSignedApprovedID, "ccla#") {
154+
isCCLA = true
155+
totalCCLA++
156+
} else {
157+
// Skip unknown types
158+
continue
159+
}
160+
} else if sig.SignatureType != "" {
161+
// Fallback method: check signature_type field
162+
switch sig.SignatureType {
163+
case "cla", "icla":
164+
// For legacy CLA records without sigtype_signed_approved_id, treat as ICLA
165+
isICLA = true
166+
totalICLA++
167+
case "ccla":
168+
isCCLA = true
169+
totalCCLA++
170+
case "ecla":
171+
isECLA = true
172+
totalECLA++
173+
default:
174+
continue
175+
}
176+
} else {
177+
// Skip records without type information
178+
continue
179+
}
180+
181+
// Initialize month stats if not exists
182+
if monthlyStats[month] == nil {
183+
monthlyStats[month] = &MonthlyStats{Month: month}
184+
}
185+
186+
// Increment appropriate counter
187+
if isICLA {
188+
monthlyStats[month].ICLA++
189+
} else if isECLA {
190+
monthlyStats[month].ECLA++
191+
} else if isCCLA {
192+
monthlyStats[month].CCLA++
193+
}
194+
}
195+
return true // Continue scanning
196+
})
197+
198+
if err != nil {
199+
log.Fatalf("Error scanning table: %v", err)
200+
}
201+
202+
fmt.Printf("\nProcessing complete!\n")
203+
fmt.Printf("Total records processed: %d\n", totalProcessed)
204+
fmt.Printf("Total ICLA signatures: %d\n", totalICLA)
205+
fmt.Printf("Total ECLA signatures: %d\n", totalECLA)
206+
fmt.Printf("Total CCLA signatures: %d\n", totalCCLA)
207+
fmt.Printf("Skipped invalid dates: %d\n", skippedInvalidDates)
208+
fmt.Printf("Skipped future dates: %d\n", skippedFutureDates)
209+
210+
// Convert map to slice and sort by month
211+
var monthlyData []MonthlyStats
212+
for _, stats := range monthlyStats {
213+
monthlyData = append(monthlyData, *stats)
214+
}
215+
216+
sort.Slice(monthlyData, func(i, j int) bool {
217+
return monthlyData[i].Month < monthlyData[j].Month
218+
})
219+
220+
// Create CSV output
221+
outputFile := "signature_monthly_report.csv"
222+
file, err := os.Create(outputFile)
223+
if err != nil {
224+
log.Fatalf("Error creating output file: %v", err)
225+
}
226+
defer file.Close()
227+
228+
writer := csv.NewWriter(file)
229+
230+
// Set semicolon as separator
231+
writer.Comma = ';'
232+
233+
// Write header
234+
if err := writer.Write([]string{"month", "ICLAs", "ECLAs", "CCLAs"}); err != nil {
235+
log.Fatalf("Error writing CSV header: %v", err)
236+
}
237+
238+
// Write data
239+
for _, stats := range monthlyData {
240+
record := []string{
241+
stats.Month,
242+
strconv.Itoa(stats.ICLA),
243+
strconv.Itoa(stats.ECLA),
244+
strconv.Itoa(stats.CCLA),
245+
}
246+
if err := writer.Write(record); err != nil {
247+
log.Fatalf("Error writing CSV record for month %s: %v", stats.Month, err)
248+
}
249+
}
250+
writer.Flush()
251+
if err := writer.Error(); err != nil {
252+
log.Fatalf("Error flushing CSV writer: %v", err)
253+
}
254+
255+
fmt.Printf("Report generated: %s\n", outputFile)
256+
fmt.Printf("Total months with activity: %d\n", len(monthlyData))
257+
}
258+
259+
// extractMonth extracts YYYY-MM from date_created field with proper validation
260+
func extractMonth(dateStr string) string {
261+
if dateStr == "" {
262+
return ""
263+
}
264+
265+
// Handle different date formats
266+
// 2021-08-09T15:21:56.492368+0000
267+
// 2024-07-30T12:11:34Z
268+
269+
var t time.Time
270+
var err error
271+
272+
// Try parsing different formats
273+
formats := []string{
274+
"2006-01-02T15:04:05.999999+0000",
275+
"2006-01-02T15:04:05Z",
276+
"2006-01-02T15:04:05.999999Z",
277+
"2006-01-02T15:04:05+0000",
278+
"2006-01-02T15:04:05.999999-0700",
279+
"2006-01-02T15:04:05-0700",
280+
time.RFC3339,
281+
time.RFC3339Nano,
282+
"2006-01-02 15:04:05",
283+
"2006-01-02",
284+
}
285+
286+
for _, format := range formats {
287+
t, err = time.Parse(format, dateStr)
288+
if err == nil {
289+
break
290+
}
291+
}
292+
293+
thisYear := time.Now().Year()
294+
if err != nil {
295+
// Try to extract just the date part
296+
parts := strings.Split(dateStr, "T")
297+
if len(parts) > 0 {
298+
datePart := parts[0]
299+
if len(datePart) >= 7 { // YYYY-MM format at minimum
300+
// Try different date part lengths
301+
for _, length := range []int{10, 7} { // YYYY-MM-DD or YYYY-MM
302+
if len(datePart) >= length {
303+
testDateStr := datePart[:length]
304+
var testFormat string
305+
if length == 10 {
306+
testFormat = "2006-01-02"
307+
} else {
308+
testFormat = "2006-01"
309+
}
310+
311+
if testTime, testErr := time.Parse(testFormat, testDateStr); testErr == nil {
312+
// Validate year and month ranges
313+
year := testTime.Year()
314+
month := int(testTime.Month())
315+
316+
if year >= 2000 && year <= thisYear &&
317+
month >= 1 && month <= 12 {
318+
return testTime.Format("2006-01")
319+
}
320+
}
321+
}
322+
}
323+
}
324+
}
325+
return ""
326+
}
327+
328+
// Validate the parsed time
329+
year := t.Year()
330+
month := int(t.Month())
331+
332+
// Check for reasonable year and month ranges
333+
if year < 2000 || year > thisYear || month < 1 || month > 12 {
334+
return ""
335+
}
336+
337+
result := t.Format("2006-01")
338+
339+
// Additional validation: don't return invalid months like 2025-26
340+
if testTime, testErr := time.Parse("2006-01", result); testErr == nil {
341+
// Ensure the month is valid
342+
if testTime.Month() >= 1 && testTime.Month() <= 12 {
343+
return result
344+
}
345+
}
346+
347+
return ""
348+
}

0 commit comments

Comments
 (0)