diff --git a/internal/etw/eventdata.go b/internal/etw/eventdata.go index 6296099..fc270e1 100644 --- a/internal/etw/eventdata.go +++ b/internal/etw/eventdata.go @@ -11,20 +11,35 @@ type EventData struct { buffer bytes.Buffer } -// NewEventData returns a new EventData with an empty buffer. -func NewEventData() *EventData { - return &EventData{} +// Bytes returns the raw binary data containing the event data. The returned +// value is not copied from the internal buffer, so it can be mutated by the +// EventData object after it is returned. +func (ed *EventData) Bytes() []byte { + return ed.buffer.Bytes() } -// AddString appends the data for a string to the end of the buffer. -func (ed *EventData) AddString(data string) { +// WriteString appends a string, including the null terminator, to the buffer. +func (ed *EventData) WriteString(data string) { ed.buffer.WriteString(data) ed.buffer.WriteByte(0) } -// This is mostly added for testing purposes, and will be removed later, as we -// shouldn't take a dependency on binary.Write knowing how to marshal values -// correctly for TraceLogging. -func (ed *EventData) AddSimple(data interface{}) { - binary.Write(&ed.buffer, binary.LittleEndian, data) +// WriteUint8 appends a uint8 to the buffer. +func (ed *EventData) WriteUint8(value uint8) { + ed.buffer.WriteByte(value) +} + +// WriteUint16 appends a uint16 to the buffer. +func (ed *EventData) WriteUint16(value uint16) { + binary.Write(&ed.buffer, binary.LittleEndian, value) +} + +// WriteUint32 appends a uint32 to the buffer. +func (ed *EventData) WriteUint32(value uint32) { + binary.Write(&ed.buffer, binary.LittleEndian, value) +} + +// WriteUint64 appends a uint64 to the buffer. +func (ed *EventData) WriteUint64(value uint64) { + binary.Write(&ed.buffer, binary.LittleEndian, value) } diff --git a/internal/etw/event.go b/internal/etw/eventdescriptor.go similarity index 75% rename from internal/etw/event.go rename to internal/etw/eventdescriptor.go index a67e62c..3946765 100644 --- a/internal/etw/event.go +++ b/internal/etw/eventdescriptor.go @@ -27,23 +27,6 @@ const ( LevelVerbose ) -// Event represents a single ETW event. It can have field metadata and data -// added to it, and then be logged via a provider to actually send it to ETW. -type Event struct { - Descriptor *EventDescriptor - Metadata *EventMetadata - Data *EventData -} - -// NewEvent returns a new instance of an event object. -func NewEvent(name string, descriptor *EventDescriptor) *Event { - return &Event{ - Descriptor: descriptor, - Metadata: NewEventMetadata(name), - Data: &EventData{}, - } -} - // EventDescriptor represents various metadata for an ETW event. type EventDescriptor struct { id uint16 @@ -61,13 +44,8 @@ func NewEventDescriptor() *EventDescriptor { // Standard TraceLogging events default to the TraceLogging channel, and // verbose level. return &EventDescriptor{ - id: 0, - version: 0, Channel: ChannelTraceLogging, Level: LevelVerbose, - Opcode: 0, - Task: 0, - Keyword: 0, } } diff --git a/internal/etw/eventmetadata.go b/internal/etw/eventmetadata.go index 4d74db9..805b756 100644 --- a/internal/etw/eventmetadata.go +++ b/internal/etw/eventmetadata.go @@ -82,15 +82,24 @@ type EventMetadata struct { buffer bytes.Buffer } -// NewEventMetadata returns a new EventMetadata with event name and initial -// metadata written to the buffer. -func NewEventMetadata(name string) *EventMetadata { - em := EventMetadata{} +// Bytes returns the raw binary data containing the event metadata. Before being +// returned, the current size of the buffer is written to the start of the +// buffer. The returned value is not copied from the internal buffer, so it can +// be mutated by the EventMetadata object after it is returned. +func (em *EventMetadata) Bytes() []byte { + // Finalize the event metadata buffer by filling in the buffer length at the + // beginning. + binary.LittleEndian.PutUint16(em.buffer.Bytes(), uint16(em.buffer.Len())) + return em.buffer.Bytes() +} + +// WriteEventHeader writes the metadata for the start of an event to the buffer. +// This specifies the event name and tags. +func (em *EventMetadata) WriteEventHeader(name string, tags uint32) { binary.Write(&em.buffer, binary.LittleEndian, uint16(0)) // Length placeholder - em.writeTags(0) + em.writeTags(tags) em.buffer.WriteString(name) em.buffer.WriteByte(0) // Null terminator for name - return &em } type field struct { @@ -150,55 +159,44 @@ func (em *EventMetadata) writeTags(tags uint32) { } } -type fieldOpt func(f *field) - -// WithOutType specifies the out type for the field. This value is used as a -// hint by the event decoder for how the field value should be formatted. If no -// out type is specified, a default formatting based on the in type will be -// used. -func WithOutType(outType OutType) fieldOpt { - return func(f *field) { - f.outType = outType - } +// WriteField writes the metadata for a simple field to the buffer. +func (em *EventMetadata) WriteField(name string, inType InType, outType OutType, tags uint32) { + em.writeField(field{ + name: name, + inType: inType, + outType: outType, + tags: tags, + }) } -// WithTags adds a tag to the field. Tags are 28-bit values that have meaning -// only to the event consumer. The top 4 bits of the value will be ignored. -// Multiple uses of this option will cause the tags to be OR'd together. -func WithTags(tags uint32) fieldOpt { - return func(f *field) { - f.tags |= tags - } +// WriteArray writes the metadata for an array field to the buffer. The number +// of elements in the array must be written as a uint16 in the event data, +// immediately preceeding the event data. +func (em *EventMetadata) WriteArray(name string, inType InType, outType OutType, tags uint32) { + em.WriteField(name, inType|InTypeArray, outType, tags) } -// WithCountedArray marks the field as being an array of a fixed number of -// elements. The number of elements is encoded directly into the field metadata. -func WithCountedArray(count uint16) fieldOpt { - return func(f *field) { - f.inType |= InTypeCountedArray - f.countedArraySize = count - } +// WriteCountedArray writes the metadata for an array field to the buffer. The +// size of a counted array is fixed, and the size is written into the metadata +// directly. +func (em *EventMetadata) WriteCountedArray(name string, count uint16, inType InType, outType OutType, tags uint32) { + em.writeField(field{ + name: name, + inType: inType | InTypeCountedArray, + outType: outType, + tags: tags, + countedArraySize: count, + }) } -// WithArray marks the field as being an array of a dynamic number of elements. -// The number of elements must be written as a uint16 to the data block, -// immediately preceeding the array elements. -func WithArray() fieldOpt { - return func(f *field) { - f.inType |= InTypeArray - } -} - -// AddField appends a single field to the end of the event metadata buffer. -func (em *EventMetadata) AddField(name string, inType InType, opts ...fieldOpt) { - f := field{ - name: name, - inType: inType, - } - - for _, opt := range opts { - opt(&f) - } - - em.writeField(f) +// WriteStruct writes the metadata for a nested struct to the buffer. The struct +// contains the next N fields in the metadata, where N is specified by the +// fieldCount argument. +func (em *EventMetadata) WriteStruct(name string, fieldCount uint8, tags uint32) { + em.writeField(field{ + name: name, + inType: InTypeStruct, + outType: OutType(fieldCount), + tags: tags, + }) } diff --git a/internal/etw/eventopt.go b/internal/etw/eventopt.go new file mode 100644 index 0000000..78b7859 --- /dev/null +++ b/internal/etw/eventopt.go @@ -0,0 +1,29 @@ +package etw + +// EventOpt defines the option function type that can be passed to +// Provider.WriteEvent to specify general event options, such as level and +// keyword. +type EventOpt func(*EventDescriptor, *uint32) + +// WithLevel specifies the level of the event to be written. +func WithLevel(level Level) EventOpt { + return func(descriptor *EventDescriptor, tags *uint32) { + descriptor.Level = level + } +} + +// WithKeyword specifies the keywords of the event to be written. Multiple uses +// of this option are OR'd together. +func WithKeyword(keyword uint64) EventOpt { + return func(descriptor *EventDescriptor, tags *uint32) { + descriptor.Keyword |= keyword + } +} + +// WithTags specifies the tags of the event to be written. Tags is a 28-bit +// value (top 4 bits are ignored) which are interpreted by the event consumer. +func WithTags(newTags uint32) EventOpt { + return func(descriptor *EventDescriptor, tags *uint32) { + *tags |= newTags + } +} diff --git a/internal/etw/fieldopt.go b/internal/etw/fieldopt.go new file mode 100644 index 0000000..03854cc --- /dev/null +++ b/internal/etw/fieldopt.go @@ -0,0 +1,35 @@ +package etw + +// FieldOpt defines the option function type that can be passed to +// Provider.WriteEvent to add fields to the event. +type FieldOpt func(em *EventMetadata, ed *EventData) + +// StringField adds a single string field to the event. +func StringField(name string, value string) FieldOpt { + return func(em *EventMetadata, ed *EventData) { + em.WriteField(name, InTypeANSIString, OutTypeUTF8, 0) + ed.WriteString(value) + } +} + +// StringArray adds an array of strings to the event. +func StringArray(name string, values []string) FieldOpt { + return func(em *EventMetadata, ed *EventData) { + em.WriteArray(name, InTypeANSIString, OutTypeUTF8, 0) + ed.WriteUint16(uint16(len(values))) + for _, v := range values { + ed.WriteString(v) + } + } +} + +// Struct adds a nested struct to the event, the FieldOpts in the opts argument +// are used to specify the fields of the struct. +func Struct(name string, opts ...FieldOpt) FieldOpt { + return func(em *EventMetadata, ed *EventData) { + em.WriteStruct(name, uint8(len(opts)), 0) + for _, opt := range opts { + opt(em, ed) + } + } +} diff --git a/internal/etw/provider.go b/internal/etw/provider.go index 880f17e..501f707 100644 --- a/internal/etw/provider.go +++ b/internal/etw/provider.go @@ -172,6 +172,8 @@ func providerIDFromName(name string) (*windows.GUID, error) { }, nil } +// NewProvider creates and registers a new ETW provider. The provider ID is +// generated based on the provider name. func NewProvider(name string, callback EnableCallback) (provider *Provider, err error) { id, err := providerIDFromName(name) if err != nil { @@ -180,7 +182,10 @@ func NewProvider(name string, callback EnableCallback) (provider *Provider, err return NewProviderWithID(name, id, callback) } -// NewProvider creates and registers a new provider. +// NewProviderWithID creates and registers a new ETW provider, allowing the +// provider ID to be manually specified. This is most useful when there is an +// existing provider ID that must be used to conform to existing diagnostic +// infrastructure. func NewProviderWithID(name string, id *windows.GUID, callback EnableCallback) (provider *Provider, err error) { provider = providers.newProvider() defer func() { @@ -246,16 +251,56 @@ func (provider *Provider) IsEnabledForLevelAndKeywords(level Level, keywords uin return true } -// WriteEvent writes a single event to ETW, from this provider. -func (provider *Provider) WriteEvent(event *Event) error { - // Finalize the event metadata buffer by filling in the buffer length at the - // beginning. - binary.LittleEndian.PutUint16(event.Metadata.buffer.Bytes(), uint16(event.Metadata.buffer.Len())) +// WriteEvent writes a single ETW event from the provider. The event is +// constructed based on the EventOpt and FieldOpt values that are passed as +// opts. +func (provider *Provider) WriteEvent(name string, opts ...interface{}) error { + tags := uint32(0) + descriptor := NewEventDescriptor() + em := &EventMetadata{} + ed := &EventData{} - var dataDescriptors [3]eventDataDescriptor - dataDescriptors[0].set(eventDataDescriptorTypeProviderMetadata, provider.metadata) - dataDescriptors[1].set(eventDataDescriptorTypeEventMetadata, event.Metadata.buffer.Bytes()) - dataDescriptors[2].set(eventDataDescriptorTypeUserData, event.Data.buffer.Bytes()) + // We need to evaluate the EventOpts first since they might change tags, and + // we write out the tags before evaluating FieldOpts. + for _, opt := range opts { + if v, ok := opt.(EventOpt); ok { + v(descriptor, &tags) + } + } - return eventWriteTransfer(provider.handle, event.Descriptor, nil, nil, 3, &dataDescriptors[0]) + em.WriteEventHeader(name, tags) + + for _, opt := range opts { + if v, ok := opt.(FieldOpt); ok { + v(em, ed) + } + } + + return provider.WriteEventRaw(descriptor, [][]byte{em.Bytes()}, [][]byte{ed.Bytes()}) +} + +// WriteEventRaw writes a single ETW event from the provider. This function is +// less abstracted than WriteEvent, and presents a fairly direct interface to +// the event writing functionality. It expects a series of event metadata and +// event data blobs to be passed in, which must conform to the TraceLogging +// schema. The functions on EventMetadata and EventData can help with creating +// these blobs. The blobs of each type are effectively concatenated together by +// the ETW infrastructure. +func (provider *Provider) WriteEventRaw(descriptor *EventDescriptor, metadataBlobs [][]byte, dataBlobs [][]byte) error { + dataDescriptorCount := uint32(1 + len(metadataBlobs) + len(dataBlobs)) + dataDescriptors := make([]eventDataDescriptor, dataDescriptorCount) + + i := 0 + dataDescriptors[i].set(eventDataDescriptorTypeProviderMetadata, provider.metadata) + i++ + for _, blob := range metadataBlobs { + dataDescriptors[i].set(eventDataDescriptorTypeEventMetadata, blob) + i++ + } + for _, blob := range dataBlobs { + dataDescriptors[i].set(eventDataDescriptorTypeUserData, blob) + i++ + } + + return eventWriteTransfer(provider.handle, descriptor, nil, nil, dataDescriptorCount, &dataDescriptors[0]) } diff --git a/internal/etw/sample/sample.go b/internal/etw/sample/sample.go index f041434..1d8d363 100644 --- a/internal/etw/sample/sample.go +++ b/internal/etw/sample/sample.go @@ -51,29 +51,56 @@ func main() { reader := bufio.NewReader(os.Stdin) - fmt.Println("Press enter to log an event") + fmt.Println("Press enter to log events") reader.ReadString('\n') - event := etw.NewEvent("TestEvent", etw.NewEventDescriptor()) - event.Metadata.AddField("TestField", etw.InTypeANSIString) - event.Data.AddString("Foo") - event.Metadata.AddField("TestField2", etw.InTypeANSIString) - event.Data.AddString("Bar") - event.Metadata.AddField("TestArray", etw.InTypeANSIString, etw.WithArray()) - event.Data.AddSimple(uint16(5)) - event.Data.AddString("Item1") - event.Data.AddString("Item2") - event.Data.AddString("Item3") - event.Data.AddString("Item4") - event.Data.AddString("Item5") - - if err := provider.WriteEvent(event); err != nil { - fmt.Println(err) + // Write using high-level API. + if err := provider.WriteEvent( + "TestEvent", + etw.WithLevel(etw.LevelInfo), + etw.WithKeyword(0x140), + etw.StringField("TestField", "Foo"), + etw.StringField("TestField2", "Bar"), + etw.Struct("TestStruct", + etw.StringField("Field1", "Value1"), + etw.StringField("Field2", "Value2")), + etw.StringArray("TestArray", []string{ + "Item1", + "Item2", + "Item3", + "Item4", + "Item5", + }), + ); err != nil { + logrus.Error(err) return } - fmt.Println("Event written") - - fmt.Println("Press enter to exit") - reader.ReadString('\n') + // Write using low-level API. + descriptor := etw.NewEventDescriptor() + descriptor.Level = etw.LevelInfo + descriptor.Keyword = 0x140 + em := &etw.EventMetadata{} + ed := &etw.EventData{} + em.WriteEventHeader("TestEvent", 0) + em.WriteField("TestField", etw.InTypeANSIString, etw.OutTypeUTF8, 0) + ed.WriteString("Foo") + em.WriteField("TestField2", etw.InTypeANSIString, etw.OutTypeUTF8, 0) + ed.WriteString("Bar") + em.WriteStruct("TestStruct", 2, 0) + em.WriteField("Field1", etw.InTypeANSIString, etw.OutTypeUTF8, 0) + ed.WriteString("Value1") + em.WriteField("Field2", etw.InTypeANSIString, etw.OutTypeUTF8, 0) + ed.WriteString("Value2") + em.WriteArray("TestArray", etw.InTypeANSIString, etw.OutTypeDefault, 0) + ed.WriteUint16(5) + ed.WriteString("Item1") + ed.WriteString("Item2") + ed.WriteString("Item3") + ed.WriteString("Item4") + ed.WriteString("Item5") + if err := provider.WriteEventRaw(descriptor, [][]byte{em.Bytes()}, [][]byte{ed.Bytes()}); err != nil { + logrus.Error(err) + return + } } diff --git a/pkg/etwlogrus/hook.go b/pkg/etwlogrus/hook.go index 294835a..7146765 100644 --- a/pkg/etwlogrus/hook.go +++ b/pkg/etwlogrus/hook.go @@ -46,30 +46,31 @@ func (h *Hook) Fire(e *logrus.Entry) error { return nil } - descriptor := etw.NewEventDescriptor() + opts := make([]interface{}, len(e.Data)) + i := 0 // We could try to map Logrus levels to ETW levels, but we would lose some // fidelity as there are fewer ETW levels. So instead we use the level // directly. - descriptor.Level = etw.Level(e.Level) + opts[i] = etw.WithLevel(etw.Level(e.Level)) + i++ - event := etw.NewEvent("LogrusEntry", descriptor) - - event.Metadata.AddField("Message", etw.InTypeANSIString, etw.WithOutType(etw.OutTypeUTF8)) - event.Data.AddString(e.Message) + opts[i] = etw.StringField("Message", e.Message) + i++ for k, v := range e.Data { switch v := v.(type) { case string: - event.Metadata.AddField(k, etw.InTypeANSIString, etw.WithOutType(etw.OutTypeUTF8)) - event.Data.AddString(v) + opts[i] = etw.StringField(k, v) default: - event.Metadata.AddField(k, etw.InTypeANSIString, etw.WithOutType(etw.OutTypeUTF8)) - event.Data.AddString(fmt.Sprintf(" %v", reflect.TypeOf(v), v)) + opts[i] = etw.StringField(k, fmt.Sprintf(" %v", reflect.TypeOf(v), v)) } + i++ } - h.provider.WriteEvent(event) + h.provider.WriteEvent( + "LogrusEntry", + opts...) return nil }