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