github.com/mentimeter/morty@v1.2.2-0.20221012065510-5596adecd154/mortems/mortem_data.go (about)

     1  package mortems
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"regexp"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  )
    11  
    12  type MortemData struct {
    13  	File     string        `json:"file"`
    14  	Title    string        `json:"title"`
    15  	Owner    string        `json:"owner"`
    16  	Date     time.Time     `json:"date"`
    17  	Severity string        `json:"severity"`
    18  	Detect   time.Duration `json:"detect"`
    19  	Resolve  time.Duration `json:"resolve"`
    20  	Downtime time.Duration `json:"total_down"`
    21  }
    22  
    23  var ErrNoTitle = errors.New("no title of format \"# Title Here\"")
    24  var ErrNoOwner = errors.New("no owner of format \"Owner: First Last\"")
    25  var ErrNoDate = errors.New("no date of format \"July 1, 2020\" (no st, nd, th)")
    26  var ErrNoSeverity = errors.New("no severity of format \"| Severity | sev |\"")
    27  var ErrNoDetect = errors.New("no time to detect of format \"| Time to Detect | x unit[, y smaller_unit] |\"")
    28  var ErrNoResolve = errors.New("no time to resolve of format \"| Time to Resolve | x unit[, y smaller_unit] |\"")
    29  var ErrNoDowntime = errors.New("no total downtime format \"| Total Downtime | x unit[, y smaller_unit] |\"")
    30  
    31  func NewMortemData(content, path string) (MortemData, error) {
    32  	title, err := ParseTitle(content)
    33  	if err != nil {
    34  		return MortemData{}, err
    35  	}
    36  
    37  	owner, err := ParseOwner(content)
    38  	if err != nil {
    39  		return MortemData{}, err
    40  	}
    41  
    42  	date, err := ParseDate(content)
    43  	if err != nil {
    44  		return MortemData{}, err
    45  	}
    46  
    47  	severity, err := ParseSeverity(content)
    48  	if err != nil {
    49  		return MortemData{}, err
    50  	}
    51  
    52  	detect, err := ParseDetect(content)
    53  	if err != nil {
    54  		return MortemData{}, err
    55  	}
    56  
    57  	resolve, err := ParseResolve(content)
    58  	if err != nil {
    59  		return MortemData{}, err
    60  	}
    61  
    62  	downtime, err := ParseDowntime(content)
    63  	if err != nil {
    64  		return MortemData{}, err
    65  	}
    66  
    67  	return MortemData{
    68  		File:     path,
    69  		Title:    title,
    70  		Owner:    owner,
    71  		Date:     date,
    72  		Severity: severity,
    73  		Detect:   detect,
    74  		Resolve:  resolve,
    75  		Downtime: downtime,
    76  	}, nil
    77  }
    78  
    79  func ParseTitle(content string) (string, error) {
    80  	re := regexp.MustCompile(`(?i)# (?P<Title>.+)`)
    81  
    82  	title := re.FindStringSubmatch(content)
    83  	if title == nil {
    84  		return "", ErrNoTitle
    85  	}
    86  
    87  	return title[1], nil
    88  }
    89  
    90  func ParseOwner(content string) (string, error) {
    91  	re := regexp.MustCompile(`(?i).*Owner: (?P<Owner>.+)`)
    92  
    93  	owner := re.FindStringSubmatch(content)
    94  	if owner == nil {
    95  		return "", ErrNoOwner
    96  	}
    97  
    98  	return owner[1], nil
    99  }
   100  
   101  func ParseDate(content string) (time.Time, error) {
   102  	re := regexp.MustCompile(`(?i).*Date: (?P<Date>.+)`)
   103  
   104  	dateStr := re.FindStringSubmatch(content)
   105  	if dateStr == nil {
   106  		return time.Now(), ErrNoDate
   107  	}
   108  
   109  	var date time.Time
   110  	var err error
   111  
   112  	date, err = time.Parse("January 2, 2006", dateStr[1])
   113  	if err != nil {
   114  		date, err = time.Parse("Jan 2, 2006", dateStr[1])
   115  		if err != nil {
   116  			return time.Now(), fmt.Errorf("incorrect date format, %s: %w", err, ErrNoDate)
   117  		}
   118  	}
   119  
   120  	return date, nil
   121  }
   122  
   123  func ParseSeverity(content string) (string, error) {
   124  	re := regexp.MustCompile(`(?i) *\| *Severity +\| *(.+) +\| *`)
   125  
   126  	sev := re.FindStringSubmatch(content)
   127  	if sev == nil {
   128  		return "", ErrNoSeverity
   129  	}
   130  
   131  	return sev[1], nil
   132  }
   133  
   134  func ParseDetect(content string) (time.Duration, error) {
   135  	re := regexp.MustCompile(`(?i) *\|.*Detect +\| *(.+) +\| *`)
   136  
   137  	detectMatches := re.FindStringSubmatch(content)
   138  	if detectMatches == nil {
   139  		return 0, ErrNoDetect
   140  	}
   141  
   142  	detectString := detectMatches[1]
   143  
   144  	detectTime, err := stringToTime(detectString)
   145  	if err != nil {
   146  		return 0, fmt.Errorf("%w: %s", ErrNoDetect, err)
   147  	}
   148  
   149  	return detectTime, nil
   150  }
   151  
   152  func ParseResolve(content string) (time.Duration, error) {
   153  	re := regexp.MustCompile(`(?i) *\|.*Resolve +\| *(.+) +\| *`)
   154  
   155  	resolveMatches := re.FindStringSubmatch(content)
   156  	if resolveMatches == nil {
   157  		return 0, ErrNoResolve
   158  	}
   159  
   160  	resolveString := resolveMatches[1]
   161  
   162  	resolveTime, err := stringToTime(resolveString)
   163  	if err != nil {
   164  		return 0, fmt.Errorf("%w: %s", ErrNoResolve, err)
   165  	}
   166  
   167  	return resolveTime, nil
   168  }
   169  
   170  func ParseDowntime(content string) (time.Duration, error) {
   171  	re := regexp.MustCompile(`(?i) *\|.*Downtime +\| *(.+) +\| *`)
   172  
   173  	downtimeMatches := re.FindStringSubmatch(content)
   174  	if downtimeMatches == nil {
   175  		return 0, ErrNoDowntime
   176  	}
   177  
   178  	downtimeString := downtimeMatches[1]
   179  
   180  	downtimeTime, err := stringToTime(downtimeString)
   181  	if err != nil {
   182  		return 0, fmt.Errorf("%w: %s", ErrNoDowntime, err)
   183  	}
   184  
   185  	return downtimeTime, nil
   186  }
   187  
   188  func stringToTime(timeStr string) (time.Duration, error) {
   189  	noSpaceDetect := strings.ReplaceAll(timeStr, " ", "")
   190  	lowerCaseDetect := strings.ToLower(noSpaceDetect)
   191  	timeGroups := strings.Split(lowerCaseDetect, ",")
   192  
   193  	totalTime := time.Duration(0)
   194  	for _, t := range timeGroups {
   195  		re := regexp.MustCompile(`[^0-9]*([0-9]+)[^0-9]*`)
   196  		goTimeString := ""
   197  
   198  		if strings.Contains(t, "day") {
   199  			timeString := re.FindStringSubmatch(t)
   200  			if timeString == nil {
   201  				return 0, errors.New("missing number of days")
   202  			}
   203  
   204  			days, err := strconv.Atoi(timeString[1])
   205  			if err != nil {
   206  				return 0, fmt.Errorf("could not parse days: %w", err)
   207  			}
   208  
   209  			hours := days * 24
   210  			goTimeString = strconv.Itoa(hours) + "h"
   211  		} else if strings.Contains(t, "hour") {
   212  			timeString := re.FindStringSubmatch(t)
   213  			if timeString == nil {
   214  				return 0, errors.New("missing number of hours")
   215  			}
   216  
   217  			goTimeString = timeString[1] + "h"
   218  		} else if strings.Contains(t, "min") {
   219  			timeString := re.FindStringSubmatch(t)
   220  			if timeString == nil {
   221  				return 0, errors.New("missing number of minutes")
   222  			}
   223  
   224  			goTimeString = timeString[1] + "m"
   225  		} else if strings.Contains(t, "sec") {
   226  			timeString := re.FindStringSubmatch(t)
   227  			if timeString == nil {
   228  				return 0, errors.New("missing number of seconds")
   229  			}
   230  
   231  			goTimeString = timeString[1] + "s"
   232  		}
   233  
   234  		thisTime, err := time.ParseDuration(goTimeString)
   235  		if err != nil {
   236  			return 0, ErrNoDetect
   237  		}
   238  
   239  		totalTime += thisTime
   240  	}
   241  
   242  	return totalTime, nil
   243  }