mirror of
https://github.com/rwinkhart/go-winio.git
synced 2026-08-28 04:46:50 -04:00
Separate ETW into low-level and high-level API
This commit is contained in:
+25
-10
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
-11
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -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("<unknown type: %v> %v", reflect.TypeOf(v), v))
|
||||
opts[i] = etw.StringField(k, fmt.Sprintf("<unknown type: %v> %v", reflect.TypeOf(v), v))
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
h.provider.WriteEvent(event)
|
||||
h.provider.WriteEvent(
|
||||
"LogrusEntry",
|
||||
opts...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user