Skip to content

Instantly share code, notes, and snippets.

@platinummonkey
Last active April 21, 2025 16:41
Show Gist options
  • Select an option

  • Save platinummonkey/2323f232e7f6ee44c742b50ebac5b279 to your computer and use it in GitHub Desktop.

Select an option

Save platinummonkey/2323f232e7f6ee44c742b50ebac5b279 to your computer and use it in GitHub Desktop.

Proto Type Counter

buf.build protobuf type counter for their pricing so you have an understanding what the costs will be

go build -o protocounter .

protocounter /path/to/your/directory

🔍 Vehicle (message) appears in 2 locations:
  • /path/to/v1/vehicle.proto
  • /path/to/v2/cars.proto

📊 Protobuf Statistics:
Proto Files:              1,234
Messages:                 2,567
Enums:                     345
RPCs:                      789

Total Types:             3,701
Duplicate Types:           156
Unique Types:            3,545
Duplicate Percentage:     4.2%

💰 Cost Analysis:
Community Plan:         Free per month
Team Plan:              $1.85K per month ($22.2K per year)
Pro Plan:               $18.5K per month ($222K per year)
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
type TypeInfo struct {
Name string
TypeKind string // "message", "enum", or "rpc"
FilePath string
}
type DuplicateInfo struct {
TypeName string
TypeKind string
Locations []string
}
type ProtoStats struct {
TotalFiles int
TotalMessages int
TotalEnums int
TotalRPCs int
TotalTypes int
DuplicateTypes int
AllTypes map[string][]TypeInfo // key is the typename only
DuplicateDetails []DuplicateInfo
}
func newProtoStats() ProtoStats {
return ProtoStats{
AllTypes: make(map[string][]TypeInfo),
DuplicateDetails: make([]DuplicateInfo, 0),
}
}
// costs per type per month - last updated 2025-04-21
const (
communityCost float64 = 0.0
teamCost float64 = 0.50
proCost float64 = 5.00
)
// humanizeNumber formats a number with comma separators and scales large numbers
func humanizeNumber(n int) string {
if n < 1000 {
return fmt.Sprintf("%d", n)
}
return fmt.Sprintf("%d,%03d", n/1000, n%1000)
}
// humanizeCurrency formats currency with appropriate scaling and precision
func humanizeCurrency(amount float64) string {
switch {
case amount >= 1000000:
return fmt.Sprintf("$%.2fM", amount/1000000)
case amount >= 1000:
return fmt.Sprintf("$%.2fK", amount/1000)
default:
return fmt.Sprintf("$%.2f", amount)
}
}
// humanizePercentage formats a percentage with appropriate precision
func humanizePercentage(percentage float64) string {
if percentage == 0 {
return "0%"
}
if percentage < 0.1 {
return "< 0.1%"
}
if percentage == 100 {
return "100%"
}
if percentage > 99.9 {
return "> 99.9%"
}
return fmt.Sprintf("%.1f%%", percentage)
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: go run main.go <directory>")
os.Exit(1)
}
rootDir := os.Args[1]
stats := scanDirectory(rootDir)
// Calculate duplicates
stats.calculateDuplicates()
// Calculate percentage
var duplicatePercentage float64
if stats.TotalTypes > 0 {
duplicatePercentage = (float64(stats.DuplicateTypes) / float64(stats.TotalTypes)) * 100
}
if len(stats.DuplicateDetails) > 0 {
fmt.Printf("\n📋 Duplicate Type Details:\n")
fmt.Printf("Found %s duplicate types across your proto files:\n", humanizeNumber(len(stats.DuplicateDetails)))
for _, dup := range stats.DuplicateDetails {
fmt.Printf("\n🔍 %s (%s) appears in %s locations:\n", dup.TypeName, dup.TypeKind, humanizeNumber(len(dup.Locations)))
for _, loc := range dup.Locations {
fmt.Printf(" • %s\n", loc)
}
}
}
fmt.Printf("\n📊 Protobuf Statistics:\n")
fmt.Printf("%-25s %s\n", "Proto Files:", humanizeNumber(stats.TotalFiles))
fmt.Printf("%-25s %s\n", "Messages:", humanizeNumber(stats.TotalMessages))
fmt.Printf("%-25s %s\n", "Enums:", humanizeNumber(stats.TotalEnums))
fmt.Printf("%-25s %s\n", "RPCs:", humanizeNumber(stats.TotalRPCs))
fmt.Printf("\n%-25s %s\n", "Total Types:", humanizeNumber(stats.TotalTypes))
fmt.Printf("%-25s %s\n", "Duplicate Types:", humanizeNumber(stats.DuplicateTypes))
fmt.Printf("%-25s %s\n", "Unique Types:", humanizeNumber(stats.TotalTypes - stats.DuplicateTypes))
fmt.Printf("%-25s %s\n", "Duplicate Percentage:", humanizePercentage(duplicatePercentage))
monthlyTeamCost := float64(stats.TotalTypes) * teamCost
monthlyProCost := float64(stats.TotalTypes) * proCost
yearlyTeamCost := monthlyTeamCost * 12
yearlyProCost := monthlyProCost * 12
fmt.Printf("\n💰 Cost Analysis:\n")
fmt.Printf("%-25s %s per month\n", "Community Plan:", "Free")
fmt.Printf("%-25s %s per month (%s per year)\n", "Team Plan:", humanizeCurrency(monthlyTeamCost), humanizeCurrency(yearlyTeamCost))
fmt.Printf("%-25s %s per month (%s per year)\n", "Pro Plan:", humanizeCurrency(monthlyProCost), humanizeCurrency(yearlyProCost))
}
func (stats *ProtoStats) calculateDuplicates() {
// Reset duplicate count and details
stats.DuplicateTypes = 0
stats.DuplicateDetails = make([]DuplicateInfo, 0)
// Check each type by its name
for typeName, typeInfos := range stats.AllTypes {
if len(typeInfos) > 1 {
stats.DuplicateTypes++
// Create duplicate info entry
dupInfo := DuplicateInfo{
TypeName: typeName,
TypeKind: typeInfos[0].TypeKind,
Locations: make([]string, 0, len(typeInfos)),
}
// Add all locations where this type appears
for _, info := range typeInfos {
dupInfo.Locations = append(dupInfo.Locations, info.FilePath)
}
stats.DuplicateDetails = append(stats.DuplicateDetails, dupInfo)
}
}
}
func scanDirectory(root string) ProtoStats {
stats := newProtoStats()
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".proto") {
stats.TotalFiles++
fileStats := analyzeProtoFile(path)
stats.TotalMessages += fileStats.TotalMessages
stats.TotalEnums += fileStats.TotalEnums
stats.TotalRPCs += fileStats.TotalRPCs
stats.TotalTypes += fileStats.TotalTypes
// Merge the type information
for key, types := range fileStats.AllTypes {
stats.AllTypes[key] = append(stats.AllTypes[key], types...)
}
}
return nil
})
if err != nil {
fmt.Printf("Error walking directory: %v\n", err)
os.Exit(1)
}
return stats
}
func analyzeProtoFile(path string) ProtoStats {
stats := newProtoStats()
file, err := os.Open(path)
if err != nil {
fmt.Printf("Error opening file %s: %v\n", path, err)
return stats
}
defer file.Close()
messageRegex := regexp.MustCompile(`^[\s]*message\s+(\w+)\s*{`)
enumRegex := regexp.MustCompile(`^[\s]*enum\s+(\w+)\s*{`)
rpcRegex := regexp.MustCompile(`^[\s]*rpc\s+(\w+)\s*\(`)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if matches := messageRegex.FindStringSubmatch(line); matches != nil {
typeName := matches[1]
stats.AllTypes[typeName] = append(stats.AllTypes[typeName], TypeInfo{
Name: typeName,
TypeKind: "message",
FilePath: path,
})
stats.TotalMessages++
stats.TotalTypes++
} else if matches := enumRegex.FindStringSubmatch(line); matches != nil {
typeName := matches[1]
stats.AllTypes[typeName] = append(stats.AllTypes[typeName], TypeInfo{
Name: typeName,
TypeKind: "enum",
FilePath: path,
})
stats.TotalEnums++
stats.TotalTypes++
} else if matches := rpcRegex.FindStringSubmatch(line); matches != nil {
typeName := matches[1]
stats.AllTypes[typeName] = append(stats.AllTypes[typeName], TypeInfo{
Name: typeName,
TypeKind: "rpc",
FilePath: path,
})
stats.TotalRPCs++
stats.TotalTypes++
}
}
if err := scanner.Err(); err != nil {
fmt.Printf("Error reading file %s: %v\n", path, err)
}
return stats
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment