Documentation
¶
Overview ¶
Package output writes FOCUS 1.3 rows to various output formats.
Package output provides writers for FOCUS cost data export targets.
Index ¶
- func ContentTypeForFormat(format string) string
- func FindMissingSlots(expected []time.Time, existing map[time.Time]bool) []time.Time
- func GenerateKey(prefix, orgID string, t time.Time, format string) string
- func GenerateSlots(start, end time.Time, granularity time.Duration) []time.Time
- func ParseKeyTimestamp(key string) (time.Time, error)
- func WriteCSV(w io.Writer, rows []focus.FocusRow) error
- func WriteCSVFile(path string, rows []focus.FocusRow) error
- func WriteParquetFile(path string, rows []focus.FocusRow) error
- func WriteParquetToWriter(w io.Writer, rows []focus.FocusRow) error
- type ParquetFocusRow
- type S3Uploader
- type S3UploaderConfig
- type TimeRange
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ContentTypeForFormat ¶
ContentTypeForFormat maps a file format name to its MIME type.
"parquet" -> "application/vnd.apache.parquet" "csv" -> "text/csv"
Unknown formats fall back to "application/octet-stream".
func FindMissingSlots ¶
FindMissingSlots returns the subset of expected slot times that are not present in the existing set. The result preserves the order of expected.
func GenerateKey ¶
GenerateKey builds the S3 object key for a given organisation, timestamp, and file format. The layout follows Hive-style partitioning so that tools such as Athena and Spark can prune efficiently:
{prefix}version=focus-1.3/year=2025/month=03/day=10/castai-focus-{orgID}-2025-03-10-14.parquet
func GenerateSlots ¶
GenerateSlots returns every granularity-aligned slot start time in the half-open interval [start, end). Times are truncated to granularity boundaries before iteration (hour-aligned for hourly, day-aligned for daily in UTC).
Example: GenerateSlots(10:17, 13:45, 1h) → [10:00, 11:00, 12:00, 13:00]
func ParseKeyTimestamp ¶
ParseKeyTimestamp extracts the slot start time from an S3 object key produced by GenerateKey. The key has the form:
{prefix}version=focus-1.3/year=YYYY/month=MM/day=DD/castai-focus-{orgId}-YYYY-MM-DD-HH.{format}
It parses the YYYY-MM-DD-HH portion from the filename component.
func WriteCSVFile ¶
WriteCSVFile writes FOCUS rows to a CSV file at the given path.
func WriteParquetFile ¶
WriteParquetFile writes FOCUS rows to a local Parquet file at the given path.
Types ¶
type ParquetFocusRow ¶
type ParquetFocusRow struct {
BilledCost float64 `parquet:"name=BilledCost, type=DOUBLE, repetitiontype=REQUIRED"`
BillingAccountId string `parquet:"name=BillingAccountId, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
BillingAccountName string `parquet:"name=BillingAccountName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
BillingCurrency string `parquet:"name=BillingCurrency, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
BillingPeriodStart int64 `parquet:"name=BillingPeriodStart, type=INT64, convertedtype=TIMESTAMP_MILLIS, repetitiontype=REQUIRED"`
BillingPeriodEnd int64 `parquet:"name=BillingPeriodEnd, type=INT64, convertedtype=TIMESTAMP_MILLIS, repetitiontype=REQUIRED"`
ChargeCategory string `parquet:"name=ChargeCategory, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
ChargeDescription string `parquet:"name=ChargeDescription, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
ChargePeriodStart int64 `parquet:"name=ChargePeriodStart, type=INT64, convertedtype=TIMESTAMP_MILLIS, repetitiontype=REQUIRED"`
ChargePeriodEnd int64 `parquet:"name=ChargePeriodEnd, type=INT64, convertedtype=TIMESTAMP_MILLIS, repetitiontype=REQUIRED"`
ContractedCost float64 `parquet:"name=ContractedCost, type=DOUBLE, repetitiontype=REQUIRED"`
EffectiveCost float64 `parquet:"name=EffectiveCost, type=DOUBLE, repetitiontype=REQUIRED"`
HostProviderName string `parquet:"name=HostProviderName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
InvoiceIssuerName string `parquet:"name=InvoiceIssuerName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
ListCost float64 `parquet:"name=ListCost, type=DOUBLE, repetitiontype=REQUIRED"`
PricingQuantity float64 `parquet:"name=PricingQuantity, type=DOUBLE, repetitiontype=REQUIRED"`
PricingUnit string `parquet:"name=PricingUnit, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
ServiceCategory string `parquet:"name=ServiceCategory, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
ServiceName string `parquet:"name=ServiceName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
ServiceProviderName string `parquet:"name=ServiceProviderName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
PublisherName string `parquet:"name=PublisherName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=REQUIRED"`
AvailabilityZone *string `parquet:"name=AvailabilityZone, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
ChargeClass *string `parquet:"name=ChargeClass, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
ChargeFrequency *string `parquet:"name=ChargeFrequency, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
ConsumedQuantity *float64 `parquet:"name=ConsumedQuantity, type=DOUBLE, repetitiontype=OPTIONAL"`
ConsumedUnit *string `parquet:"name=ConsumedUnit, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
InvoiceId *string `parquet:"name=InvoiceId, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
PricingCategory *string `parquet:"name=PricingCategory, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
RegionId *string `parquet:"name=RegionId, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
RegionName *string `parquet:"name=RegionName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
ResourceId *string `parquet:"name=ResourceId, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
ResourceName *string `parquet:"name=ResourceName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
ResourceType *string `parquet:"name=ResourceType, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
ServiceSubcategory *string `parquet:"name=ServiceSubcategory, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
SkuId *string `parquet:"name=SkuId, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
SkuPriceId *string `parquet:"name=SkuPriceId, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
SubAccountId *string `parquet:"name=SubAccountId, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
SubAccountName *string `parquet:"name=SubAccountName, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
ContractedUnitPrice *float64 `parquet:"name=ContractedUnitPrice, type=DOUBLE, repetitiontype=OPTIONAL"`
ListUnitPrice *float64 `parquet:"name=ListUnitPrice, type=DOUBLE, repetitiontype=OPTIONAL"`
Tags *string `parquet:"name=Tags, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
XCastAIClusterId *string `parquet:"name=x_CastAIClusterId, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
XCastAIWorkloadType *string `parquet:"name=x_CastAIWorkloadType, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
XCastAINamespace *string `parquet:"name=x_CastAINamespace, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
XCastAIPricingTier *string `parquet:"name=x_CastAIPricingTier, type=BYTE_ARRAY, convertedtype=UTF8, repetitiontype=OPTIONAL"`
XCastAICpuCost *float64 `parquet:"name=x_CastAICpuCost, type=DOUBLE, repetitiontype=OPTIONAL"`
XCastAIRamCost *float64 `parquet:"name=x_CastAIRamCost, type=DOUBLE, repetitiontype=OPTIONAL"`
XCastAIGpuCost *float64 `parquet:"name=x_CastAIGpuCost, type=DOUBLE, repetitiontype=OPTIONAL"`
XCastAIStorageCost *float64 `parquet:"name=x_CastAIStorageCost, type=DOUBLE, repetitiontype=OPTIONAL"`
}
ParquetFocusRow is a flat, Parquet-friendly representation of a FOCUS 1.3 row. Required fields are value types; optional fields are pointers so parquet-go can encode them with OPTIONAL repetition type.
type S3Uploader ¶
type S3Uploader struct {
// contains filtered or unexported fields
}
S3Uploader uploads FOCUS export files (Parquet or CSV) to an S3 bucket.
func NewS3Uploader ¶
func NewS3Uploader(ctx context.Context, cfg S3UploaderConfig) (*S3Uploader, error)
NewS3Uploader creates an S3Uploader from the supplied configuration.
When AccessKeyID and SecretAccessKey are provided, static credentials are used. Otherwise the default credential chain is consulted, allowing transparent use of IRSA, environment variables, or EC2 instance profiles.
func (*S3Uploader) ListKeys ¶
ListKeys returns all S3 object keys under the given prefix. It handles pagination transparently via the continuation token. This is used to discover which time slots already have exported files in S3.
func (*S3Uploader) Upload ¶
func (u *S3Uploader) Upload(ctx context.Context, key string, data io.Reader, contentType string) error
Upload writes the contents of data to the specified S3 key. The caller must supply a valid MIME type in contentType (see ContentTypeForFormat).
func (*S3Uploader) UploadFile ¶
UploadFile uploads a local file to S3 under the given key. The content type is inferred from the file extension.
type S3UploaderConfig ¶
type S3UploaderConfig struct {
Bucket string
Region string
Prefix string
AccessKeyID string
SecretAccessKey string
}
S3UploaderConfig holds the settings needed to create an S3Uploader.
If AccessKeyID and SecretAccessKey are both set, static credentials are used. Otherwise the default AWS credential chain is used, which supports IRSA, environment variables, EC2 instance profiles, and more.
type TimeRange ¶
TimeRange represents a contiguous half-open time interval [Start, End).
func CoalesceSlots ¶
CoalesceSlots merges a sorted slice of missing slot start times into the smallest set of contiguous TimeRange values. Two slots are considered contiguous when one immediately follows the other (i.e. the gap between them equals granularity exactly).
The returned ranges are half-open: End = last slot start + granularity.
Example: slots [10:00, 11:00, 12:00, 15:00, 16:00] with 1h granularity → [{10:00, 13:00}, {15:00, 17:00}]